From b9cd78a8e127a379bc8838c63b5e2d87927faada Mon Sep 17 00:00:00 2001 From: Tudor Date: Thu, 17 Jul 2025 17:38:59 +0300 Subject: [PATCH 01/38] tests updated --- .../BatchControoledSending.spec.ts | 6 +- .../KeystoreScreen/BatchSignAndBatch.spec.ts | 6 +- .../KeystoreScreen/BatchTransactions.spec.ts | 10 +- tests/KeystoreScreen/PingPongAbiSign.spec.ts | 2 +- tests/KeystoreScreen/PingPongManual.spec.ts | 2 +- tests/KeystoreScreen/PingPongService.spec.ts | 2 +- tests/KeystoreScreen/SignMsgAndLogout.spec.ts | 26 +- .../CancelTransactionFromWallet.spec.ts | 6 +- .../CloseTemplateModal.spec.ts | 10 +- tests/NegativeScripts/CloseWalletTab.spec.ts | 28 +- tests/NegativeScripts/NotConfirmPass.spec.ts | 20 +- tests/NegativeScripts/actions.ts | 24 +- tests/PemScreen/SignTransactionWPem.spec.ts | 2 +- tests/UrlTests/SignAndBatch.spec.ts | 35 --- tests/UrlTests/SignWithPem.spec.ts | 39 --- tests/utils/actions.ts | 239 +++++++++--------- tests/utils/enums.ts | 11 +- 17 files changed, 216 insertions(+), 252 deletions(-) delete mode 100644 tests/UrlTests/SignAndBatch.spec.ts delete mode 100644 tests/UrlTests/SignWithPem.spec.ts diff --git a/tests/KeystoreScreen/BatchControoledSending.spec.ts b/tests/KeystoreScreen/BatchControoledSending.spec.ts index 2baeaf47..69b1c50e 100644 --- a/tests/KeystoreScreen/BatchControoledSending.spec.ts +++ b/tests/KeystoreScreen/BatchControoledSending.spec.ts @@ -6,8 +6,8 @@ import { login } from '../utils/actions'; import { - GlobalSelectorEnum, GlobalDataEnum, + GlobalSelectorEnum, WalletAdressEnum } from '../utils/enums'; @@ -25,9 +25,7 @@ test.describe('Batch controlled sending', () => { await login(page, loginData); await batchTransactions(page, GlobalSelectorEnum.controlledSendingType); await page.waitForTimeout(8000); - await handlePopup(page, () => - page.locator(GlobalSelectorEnum.lastBatchControlledTransaction).click() - ); + await handlePopup(page); await page.close(); }); }); diff --git a/tests/KeystoreScreen/BatchSignAndBatch.spec.ts b/tests/KeystoreScreen/BatchSignAndBatch.spec.ts index ffe4ad55..5d38b893 100644 --- a/tests/KeystoreScreen/BatchSignAndBatch.spec.ts +++ b/tests/KeystoreScreen/BatchSignAndBatch.spec.ts @@ -6,8 +6,8 @@ import { login } from '../utils/actions'; import { - GlobalSelectorEnum, GlobalDataEnum, + GlobalSelectorEnum, WalletAdressEnum } from '../utils/enums'; @@ -25,9 +25,7 @@ test.describe('sign and batch', () => { await login(page, loginData); await batchTransactions(page, GlobalSelectorEnum.signAndBatchType); await page.waitForTimeout(8000); - await handlePopup(page, () => - page.locator(GlobalSelectorEnum.lastBatchControlledTransaction).click() - ); + await handlePopup(page); await page.close(); }); }); diff --git a/tests/KeystoreScreen/BatchTransactions.spec.ts b/tests/KeystoreScreen/BatchTransactions.spec.ts index bbc5ecbc..487de5a9 100644 --- a/tests/KeystoreScreen/BatchTransactions.spec.ts +++ b/tests/KeystoreScreen/BatchTransactions.spec.ts @@ -3,12 +3,11 @@ import { accessDapp, batchTransactions, handlePopup, - login, - pingPongHandler + login } from '../utils/actions'; import { - GlobalSelectorEnum, GlobalDataEnum, + GlobalSelectorEnum, WalletAdressEnum } from '../utils/enums'; @@ -25,10 +24,7 @@ test.describe('Batch transaction test', () => { }; await login(page, loginData); await batchTransactions(page, GlobalSelectorEnum.swapLockType); - await page.waitForTimeout(8000); - await handlePopup(page, () => - page.locator(GlobalSelectorEnum.lastBatchTransaction).click() - ); + await handlePopup(page); await page.close(); }); }); diff --git a/tests/KeystoreScreen/PingPongAbiSign.spec.ts b/tests/KeystoreScreen/PingPongAbiSign.spec.ts index 60f2939d..b98574ac 100644 --- a/tests/KeystoreScreen/PingPongAbiSign.spec.ts +++ b/tests/KeystoreScreen/PingPongAbiSign.spec.ts @@ -1,8 +1,8 @@ import { test } from '@playwright/test'; import { accessDapp, login, pingPongHandler } from '../utils/actions'; import { - GlobalSelectorEnum, GlobalDataEnum, + GlobalSelectorEnum, WalletAdressEnum } from '../utils/enums'; diff --git a/tests/KeystoreScreen/PingPongManual.spec.ts b/tests/KeystoreScreen/PingPongManual.spec.ts index d2ff7bb8..c9777d1d 100644 --- a/tests/KeystoreScreen/PingPongManual.spec.ts +++ b/tests/KeystoreScreen/PingPongManual.spec.ts @@ -1,8 +1,8 @@ import { test } from '@playwright/test'; import { accessDapp, login, pingPongHandler } from '../utils/actions'; import { - GlobalSelectorEnum, GlobalDataEnum, + GlobalSelectorEnum, WalletAdressEnum } from '../utils/enums'; diff --git a/tests/KeystoreScreen/PingPongService.spec.ts b/tests/KeystoreScreen/PingPongService.spec.ts index 76f47b75..7d35b5cc 100644 --- a/tests/KeystoreScreen/PingPongService.spec.ts +++ b/tests/KeystoreScreen/PingPongService.spec.ts @@ -1,8 +1,8 @@ import { test } from '@playwright/test'; import { accessDapp, login, pingPongHandler } from '../utils/actions'; import { - GlobalSelectorEnum, GlobalDataEnum, + GlobalSelectorEnum, WalletAdressEnum } from '../utils/enums'; diff --git a/tests/KeystoreScreen/SignMsgAndLogout.spec.ts b/tests/KeystoreScreen/SignMsgAndLogout.spec.ts index 6e8b406c..918e9638 100644 --- a/tests/KeystoreScreen/SignMsgAndLogout.spec.ts +++ b/tests/KeystoreScreen/SignMsgAndLogout.spec.ts @@ -1,8 +1,14 @@ -import { test, expect } from '@playwright/test'; -import { accessDapp, confirmPass, login } from '../utils/actions'; +import { expect, test } from '@playwright/test'; +import { + accessDapp, + confirmPass, + findPageByUrlSubstring, + login +} from '../utils/actions'; import { - GlobalSelectorEnum, GlobalDataEnum, + GlobalSelectorEnum, + OriginPageEnum, WalletAdressEnum } from '../utils/enums'; @@ -19,18 +25,16 @@ test.describe('should sign message', () => { }; await login(page, loginData); await page.getByPlaceholder('Write message here').fill('Test msg ###'); - const [walletPage] = await Promise.all([ - page.waitForEvent('popup'), - page.locator(GlobalSelectorEnum.signMsgBtn).click() - ]); + await page.locator(GlobalSelectorEnum.signMsgBtn).click(); + const walletPage = await findPageByUrlSubstring( + page, + OriginPageEnum.multiversxWallet + ); await confirmPass(walletPage); await walletPage.getByTestId(GlobalSelectorEnum.signButton).click(); - await expect( - page.locator('#sign-message div').filter({ hasText: 'Signature:' }).nth(1) - ).toBeVisible(); await expect(page.getByText('Test msg ###')).toBeVisible(); await page.getByRole('button', { name: 'Close' }).click(); - await expect(page.getByText('LoginChoose a login method')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Connect' })).toBeVisible(); await page.close(); }); }); diff --git a/tests/NegativeScripts/CancelTransactionFromWallet.spec.ts b/tests/NegativeScripts/CancelTransactionFromWallet.spec.ts index 3cd853ea..9a2aa470 100644 --- a/tests/NegativeScripts/CancelTransactionFromWallet.spec.ts +++ b/tests/NegativeScripts/CancelTransactionFromWallet.spec.ts @@ -1,8 +1,8 @@ -import { test, expect } from '@playwright/test'; -import { accessDapp, login, pingPongHandler } from '../utils/actions'; +import { expect, test } from '@playwright/test'; +import { accessDapp, login } from '../utils/actions'; import { - GlobalSelectorEnum, GlobalDataEnum, + GlobalSelectorEnum, WalletAdressEnum } from '../utils/enums'; import { closeTransaction } from './actions'; diff --git a/tests/NegativeScripts/CloseTemplateModal.spec.ts b/tests/NegativeScripts/CloseTemplateModal.spec.ts index f1349d03..f2bc97d6 100644 --- a/tests/NegativeScripts/CloseTemplateModal.spec.ts +++ b/tests/NegativeScripts/CloseTemplateModal.spec.ts @@ -1,8 +1,8 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { accessDapp, initTransaction, login } from '../utils/actions'; import { - GlobalSelectorEnum, GlobalDataEnum, + GlobalSelectorEnum, WalletAdressEnum } from '../utils/enums'; @@ -19,7 +19,11 @@ test.describe('cancel transaction from template window', () => { }; await login(page, loginData); await initTransaction(page); - await page.getByTestId(GlobalSelectorEnum.closeButton).click(); + await page.waitForTimeout(5000); + await page + .locator(GlobalSelectorEnum.closePoupBtn) + .getByRole('button', { name: 'Close' }) + .click(); await expect( page.getByText(GlobalDataEnum.transactionCanceled) ).toBeVisible(); diff --git a/tests/NegativeScripts/CloseWalletTab.spec.ts b/tests/NegativeScripts/CloseWalletTab.spec.ts index 0a6d0c6b..ee448226 100644 --- a/tests/NegativeScripts/CloseWalletTab.spec.ts +++ b/tests/NegativeScripts/CloseWalletTab.spec.ts @@ -1,8 +1,14 @@ -import { test, expect } from '@playwright/test'; -import { accessDapp, initTransaction, login } from '../utils/actions'; +import { expect, test } from '@playwright/test'; +import { + accessDapp, + findPageByUrlSubstring, + initTransaction, + login +} from '../utils/actions'; import { - GlobalSelectorEnum, GlobalDataEnum, + GlobalSelectorEnum, + OriginPageEnum, WalletAdressEnum } from '../utils/enums'; @@ -11,22 +17,22 @@ test.describe('test refresh window', () => { await accessDapp(page); }); - test('reload and wallet window and close it', async ({ - page, - browser, - context - }) => { + test('reload and wallet window and close it', async ({ page, context }) => { const loginData = { selector: GlobalSelectorEnum.keystoreBtn, file: GlobalDataEnum.keystoreFile, address: WalletAdressEnum.adress3 }; await login(page, loginData); - const walletpage = await initTransaction(page); + await initTransaction(page); + const wallePage = await findPageByUrlSubstring( + page, + OriginPageEnum.multiversxWallet + ); await page.waitForTimeout(3000); try { - await walletpage.reload(); - await walletpage.waitForLoadState('load'); + await wallePage.reload(); + await wallePage.waitForLoadState('load'); } catch (error) { console.error('Error during page reload:', error); } diff --git a/tests/NegativeScripts/NotConfirmPass.spec.ts b/tests/NegativeScripts/NotConfirmPass.spec.ts index 2f801e1a..169ead1b 100644 --- a/tests/NegativeScripts/NotConfirmPass.spec.ts +++ b/tests/NegativeScripts/NotConfirmPass.spec.ts @@ -1,8 +1,9 @@ -import { test, expect } from '@playwright/test'; -import { accessDapp, initTransaction, login } from '../utils/actions'; +import { expect, test } from '@playwright/test'; +import { accessDapp, findPageByUrlSubstring, login } from '../utils/actions'; import { - GlobalSelectorEnum, GlobalDataEnum, + GlobalSelectorEnum, + OriginPageEnum, WalletAdressEnum } from '../utils/enums'; @@ -11,17 +12,22 @@ test.describe('test invalid password', () => { await accessDapp(page); }); - test('3 invalid password', async ({ page, browser, context }) => { + test('3 invalid password', async ({ page }) => { const loginData = { selector: GlobalSelectorEnum.keystoreBtn, file: GlobalDataEnum.keystoreFile, address: WalletAdressEnum.adress3 }; await login(page, loginData); - const walletpage = await initTransaction(page); + await page.getByTestId(GlobalSelectorEnum.signAndBatchType).click(); + const walletPage = await findPageByUrlSubstring( + page, + OriginPageEnum.multiversxWallet + ); + for (let i = 0; i < 3; i++) { - await walletpage.getByTestId(GlobalSelectorEnum.accesPass).fill('test'); - await walletpage.locator(GlobalSelectorEnum.accesWalletBtn).click(); + await walletPage.getByTestId(GlobalSelectorEnum.accesPass).fill('test'); + await walletPage.locator(GlobalSelectorEnum.accesWalletBtn).click(); } await expect( page.getByText(GlobalDataEnum.transactionCanceled) diff --git a/tests/NegativeScripts/actions.ts b/tests/NegativeScripts/actions.ts index 8e8d2bb1..c2bb7f03 100644 --- a/tests/NegativeScripts/actions.ts +++ b/tests/NegativeScripts/actions.ts @@ -1,13 +1,10 @@ import { expect, Page } from '@playwright/test'; -import { initTransaction, confirmPass } from '../utils/actions'; -import { GlobalSelectorEnum } from '../utils/enums'; - -export const closeTransaction = async (page: Page) => { - const walletPage = await initTransaction(page); - await walletPage - .getByTestId(GlobalSelectorEnum.keystoreCloseModalBtn) - .click(); -}; +import { + confirmPass, + findPageByUrlSubstring, + initTransaction +} from '../utils/actions'; +import { GlobalSelectorEnum, OriginPageEnum } from '../utils/enums'; export const closeTemplateModal = async (page: Page) => { await initTransaction(page); @@ -25,6 +22,15 @@ export const closeTemplateModal = async (page: Page) => { await expect(page.locator(GlobalSelectorEnum.accesWalletBtn)).toBeVisible(); }; +export const closeTransaction = async (page: Page) => { + await initTransaction(page); + const wallePage = await findPageByUrlSubstring( + page, + OriginPageEnum.multiversxWallet + ); + await wallePage.getByTestId(GlobalSelectorEnum.keystoreCloseModalBtn).click(); +}; + export const closeWalletTab = async (page: Page) => { await initTransaction(page); await page.close(); diff --git a/tests/PemScreen/SignTransactionWPem.spec.ts b/tests/PemScreen/SignTransactionWPem.spec.ts index bfc36369..6fc969cc 100644 --- a/tests/PemScreen/SignTransactionWPem.spec.ts +++ b/tests/PemScreen/SignTransactionWPem.spec.ts @@ -1,8 +1,8 @@ import { test } from '@playwright/test'; import { accessDapp, login, pingPongHandler } from '../utils/actions'; import { - GlobalSelectorEnum, GlobalDataEnum, + GlobalSelectorEnum, WalletAdressEnum } from '../utils/enums'; diff --git a/tests/UrlTests/SignAndBatch.spec.ts b/tests/UrlTests/SignAndBatch.spec.ts deleted file mode 100644 index 2503937b..00000000 --- a/tests/UrlTests/SignAndBatch.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { test } from '@playwright/test'; -import { accessDapp, confirmPass, handlePopup, login } from '../utils/actions'; -import { - GlobalSelectorEnum, - GlobalDataEnum, - WalletAdressEnum -} from '../utils/enums'; - -test.describe('Batch controlled sending via url', () => { - test.beforeEach(async ({ page }) => { - await accessDapp(page); - }); - - test('should sign sign & batch controlled sending via url ', async ({ - page - }) => { - const loginData = { - selector: GlobalSelectorEnum.keystoreBtn, - file: GlobalDataEnum.keystoreFile, - address: WalletAdressEnum.adress7, - urlConnect: true - }; - await login(page, loginData); - await page.getByTestId(GlobalSelectorEnum.controlledSendingType).click(); - await confirmPass(page); - for (let i = 0; i < 5; i++) { - await page.waitForTimeout(500); - await page.click(GlobalSelectorEnum.signBtn); - } - await handlePopup(page, () => - page.locator(GlobalSelectorEnum.lastBatchControlledTransaction).click() - ); - await page.close(); - }); -}); diff --git a/tests/UrlTests/SignWithPem.spec.ts b/tests/UrlTests/SignWithPem.spec.ts deleted file mode 100644 index e44c2a35..00000000 --- a/tests/UrlTests/SignWithPem.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { test } from '@playwright/test'; -import { - accessDapp, - confirmPem, - handlePopup, - login, - pingPongHandler -} from '../utils/actions'; -import { - GlobalSelectorEnum, - GlobalDataEnum, - WalletAdressEnum -} from '../utils/enums'; - -test.describe('Ping & Pong service test with pem via url', () => { - test.beforeEach(async ({ page }) => { - await accessDapp(page); - }); - - test('sign with pem', async ({ page }) => { - const loginData = { - selector: GlobalSelectorEnum.pemBtn, - file: GlobalDataEnum.pemFile, - address: WalletAdressEnum.adress3, - urlConnect: true - }; - await login(page, loginData); - await page.getByTestId(GlobalSelectorEnum.controlledSendingType).click(); - await confirmPem(page, GlobalDataEnum.pemFile); - for (let i = 0; i < 5; i++) { - await page.waitForTimeout(500); - await page.click(GlobalSelectorEnum.signBtn); - } - await handlePopup(page, () => - page.locator(GlobalSelectorEnum.lastBatchControlledTransaction).click() - ); - await page.close(); - }); -}); diff --git a/tests/utils/actions.ts b/tests/utils/actions.ts index c5ba8e73..94b50c3e 100644 --- a/tests/utils/actions.ts +++ b/tests/utils/actions.ts @@ -1,20 +1,45 @@ import { expect, Page } from '@playwright/test'; -import { - GlobalDataEnum, - GlobalSelectorEnum, - TransactionIndexEnum -} from './enums'; -import { url } from 'inspector'; - -export const handlePopup = async (page: Page, triggerPopupAction) => { - await page.waitForTimeout(3000); - const [newPage] = await Promise.all([ - page.waitForEvent('popup'), - triggerPopupAction() - ]); - await newPage.waitForLoadState(); - await expect(newPage.getByText('Success')).toBeVisible({ timeout: 90000 }); - await newPage.close(); +import { GlobalDataEnum, GlobalSelectorEnum, OriginPageEnum } from './enums'; + +// Access Daap +export const accessDapp = async (page: Page) => { + await page.goto('/'); + await page.getByRole('button', { name: 'Connect' }).click(); +}; + +// Batch Transactions +export const batchTransactions = async ( + page: Page, + transactionType: string +) => { + const selector = page.locator(`[data-testid="${transactionType}"]`); + let i = transactionType === 'swap-lock' ? 1 : 0; + await selector.click(); + const walletPage = await findPageByUrlSubstring( + page, + OriginPageEnum.multiversxWallet + ); + await page.waitForTimeout(5000); + await confirmPass(walletPage); + for (i; i < 5; i++) { + await page.waitForTimeout(500); + await walletPage.click(GlobalSelectorEnum.signBtn); + } + await walletPage.close(); +}; + +// Check URL +export const checkUrl = async (page: Page, path: string) => { + const url = page.url(); + expect(url).toContain(path); +}; + +// Confirm Pass +export const confirmPass = async (page: Page) => { + await page + .getByTestId(GlobalSelectorEnum.accesPass) + .fill(GlobalDataEnum.globalPassword); + await page.click(GlobalSelectorEnum.accesWalletBtn); }; // Confirm PEM File @@ -23,10 +48,45 @@ export const confirmPem = async (page: Page, pemFile: string) => { await page.click(GlobalSelectorEnum.accesWalletBtn); }; -// Upload File -export const uploadFile = async (page: Page, file: string) => { - await page.getByText('Select a file').click(); - await page.setInputFiles(GlobalSelectorEnum.inputFile, file); +// Confirm Transaction +export const confirmTransaction = async (page: Page) => { + await page.click(GlobalSelectorEnum.signBtn); + await page.close(); +}; + +/** + * Finds and returns a page from the context whose URL includes the given substring. + * Throws an error if no such page is found. + */ +export const findPageByUrlSubstring = async (page: Page, urlSubstring) => { + await page.waitForTimeout(3000); + const allPages = await page.context().pages(); + const foundPage = allPages.find((p) => p.url().includes(urlSubstring)); + if (!foundPage) { + throw new Error(`No page found with URL containing: ${urlSubstring}`); + } + return foundPage; +}; + +export const handlePopup = async (page: Page) => { + await page.waitForTimeout(3000); + await page.getByRole('button', { name: 'View All' }).click(); + await page.locator(GlobalSelectorEnum.transactionLink).first().click(); + const explorerPage = findPageByUrlSubstring( + page, + OriginPageEnum.explorerPage + ); + + await (await explorerPage).waitForLoadState(); + await expect((await explorerPage).getByText('Success')).toBeVisible({ + timeout: 90000 + }); + await (await explorerPage).close(); +}; + +// Init Transaction +export const initTransaction = async (page: Page) => { + await page.getByTestId(GlobalSelectorEnum.signAndBatchType).click(); }; // Login @@ -49,12 +109,16 @@ export const login = async ( .click(); await page.locator(GlobalSelectorEnum.legacyDropdownValue).nth(1).click(); } else { - [walletPage] = await Promise.all([ - page.waitForEvent('popup'), - page.click(GlobalSelectorEnum.crossWindowLoginBtn) - ]); + await page + .locator('div') + .filter({ hasText: /^MultiversX Web Wallet$/ }) + .first() + .click(); } - + walletPage = await findPageByUrlSubstring( + page, + OriginPageEnum.multiversxWallet + ); await walletPage.click(payload.selector); await uploadFile(walletPage, payload.file); @@ -70,42 +134,6 @@ export const login = async ( } }; -// Validate Toast -export const validateToast = async (page: Page, selector: string) => { - const toast = page.locator(`[data-testid*=${selector}]`); - const text = await toast.innerText(); - expect(text).toContain('Processing'); -}; - -// Check URL -export const checkUrl = async (page: Page, path: string) => { - const url = page.url(); - expect(url).toContain(path); -}; - -// Init Transaction -export const initTransaction = async (page: Page) => { - const [walletPage] = await Promise.all([ - page.waitForEvent('popup'), - page.getByTestId(GlobalSelectorEnum.signAndBatchType).click() - ]); - return walletPage; -}; - -// Confirm Pass -export const confirmPass = async (page: Page) => { - await page - .getByTestId(GlobalSelectorEnum.accesPass) - .fill(GlobalDataEnum.globalPassword); - await page.click(GlobalSelectorEnum.accesWalletBtn); -}; - -// Confirm Transaction -export const confirmTransaction = async (page: Page) => { - await page.click(GlobalSelectorEnum.signBtn); - await page.close(); -}; - // Logout export const logout = async (page: Page) => { await page.goto(GlobalDataEnum.daapWindow); @@ -125,21 +153,6 @@ export const openProviderModal = async ( await walletPage.close(); }; -// Validate Transaction -export const validateTransaction = async (page: Page, svgIndex: number) => { - const svgElements = page.locator( - 'svg[data-icon="arrow-up-right-from-square"]' - ); - const [explorerPage] = await Promise.all([ - page.waitForEvent('popup'), - svgElements.nth(svgIndex).click() - ]); - // const explorerPage = await page.context().waitForEvent('page'); - await explorerPage - .locator('span*=Success') - .waitFor({ state: 'visible', timeout: 85000 }); -}; - // Ping Pong Handler export const pingPongHandler = async ( page: Page, @@ -156,18 +169,6 @@ export const pingPongHandler = async ( } }; -// Access Daap -export const accessDapp = async (page: Page) => { - await page.goto('/'); - await page.getByRole('link', { name: 'Connect' }).click(); -}; - -// SC Transaction -interface scTransaction { - page: Page; - type: string; - provider?: string; -} export const scTransaction = async ({ page, type, @@ -180,10 +181,11 @@ export const scTransaction = async ({ return; } await page.waitForTimeout(3000); - const [walletPage] = await Promise.all([ - page.waitForEvent('popup'), - btn.click() - ]); + await btn.click(); + const walletPage = await findPageByUrlSubstring( + page, + OriginPageEnum.multiversxWallet + ); if (provider === 'pem') { await confirmPem(walletPage, GlobalDataEnum.pemFile); } else { @@ -191,32 +193,41 @@ export const scTransaction = async ({ } await walletPage.click(GlobalSelectorEnum.signBtn); - await handlePopup(page, () => - page - .getByTestId('transactionDetailsToastBody') - .getByRole('link') - .nth(1) - .click() - ); + await handlePopup(page); await walletPage.close(); }; -// Batch Transactions -export const batchTransactions = async ( - page: Page, - transactionType: string -) => { - const selector = page.locator(`[data-testid="${transactionType}"]`); - let i = transactionType === 'swap-lock' ? 1 : 0; +// SC Transaction +interface scTransaction { + page: Page; + type: string; + provider?: string; +} + +// Upload File +export const uploadFile = async (page: Page, file: string) => { + await page.getByText('Select a file').click(); + await page.setInputFiles(GlobalSelectorEnum.inputFile, file); +}; + +// Validate Toast +export const validateToast = async (page: Page, selector: string) => { + const toast = page.locator(`[data-testid*=${selector}]`); + const text = await toast.innerText(); + expect(text).toContain('Processing'); +}; - const [walletPage] = await Promise.all([ +// Validate Transaction +export const validateTransaction = async (page: Page, svgIndex: number) => { + const svgElements = page.locator( + 'svg[data-icon="arrow-up-right-from-square"]' + ); + const [explorerPage] = await Promise.all([ page.waitForEvent('popup'), - selector.click() + svgElements.nth(svgIndex).click() ]); - await confirmPass(walletPage); - for (i; i < 5; i++) { - await page.waitForTimeout(500); - await walletPage.click(GlobalSelectorEnum.signBtn); - } - await walletPage.close(); + // const explorerPage = await page.context().waitForEvent('page'); + await explorerPage + .locator('span*=Success') + .waitFor({ state: 'visible', timeout: 85000 }); }; diff --git a/tests/utils/enums.ts b/tests/utils/enums.ts index 37b63fe0..141b303f 100644 --- a/tests/utils/enums.ts +++ b/tests/utils/enums.ts @@ -7,6 +7,7 @@ export enum GlobalDataEnum { signature = '94ea7c3c28c2e718cbac76d00423e2cfbde63017120e5353c17b675fb7d78a1e855ee55e41f021d40dad1487db11f5e897c314aaff7d4cc4dc82062abaa97403', address = 'erd16xlzk48ftvhxp8dyq6d0kkfpgpfechlzycfm9xmdmwna66pvkymqvz4vzq', signedMsgPlaceHolder = 'Insert or paste the message that was signed.', + // eslint-disable-next-line quotes signaturePlaceHolder = "Insert or paste the signature. You don't have to add '0x' prefix.", confirmationMsg = 'Valid', invalidMsg = 'Invalid', @@ -65,6 +66,7 @@ export enum GlobalSelectorEnum { ledgerBtn = '[data-testid="ledgerLoginButton"]', pemBtn = '[data-testid="pemBtn"]', modalCloseBtn = '[data-testid="modalCloseButton"]', + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values closeTemplateModal = 'button*=Close', confirmParagraph = '//*[@id="dapp-modal"]/div/div/div/p', closeBtn = '[data-testid="closeButton"]', @@ -86,7 +88,14 @@ export enum GlobalSelectorEnum { lastBatchTransaction = 'div:nth-child(5) > .dapp-core-component__explorerLinkStyles__link', lastBatchControlledTransaction = 'div:nth-child(6) > .dapp-core-component__explorerLinkStyles__link', closeButton = 'closeButton', - signButton = 'signButton' + signButton = 'signButton', + transactionLink = '.transaction-link', + closePoupBtn = 'mvx-provider-idle-screen' +} + +export enum OriginPageEnum { + multiversxWallet = 'wallet.multiversx.com', + explorerPage = 'devnet-explorer.multiversx.com' } export enum RoutesEnum { From 56e1b33b04260b256cd4a783eb07e54f67456698 Mon Sep 17 00:00:00 2001 From: Tudor Date: Thu, 17 Jul 2025 17:51:40 +0300 Subject: [PATCH 02/38] Test signed commit --- test.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 test.txt diff --git a/test.txt b/test.txt new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/test.txt @@ -0,0 +1 @@ +test From a488a1689f496e6d25f707312c9dc6f54bd74115 Mon Sep 17 00:00:00 2001 From: Tudor Date: Thu, 17 Jul 2025 17:52:31 +0300 Subject: [PATCH 03/38] th/update/e2e-tests --- tests/utils/enums.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/utils/enums.ts b/tests/utils/enums.ts index 141b303f..b261e430 100644 --- a/tests/utils/enums.ts +++ b/tests/utils/enums.ts @@ -90,7 +90,8 @@ export enum GlobalSelectorEnum { closeButton = 'closeButton', signButton = 'signButton', transactionLink = '.transaction-link', - closePoupBtn = 'mvx-provider-idle-screen' + closePoupBtn = 'mvx-provider-idle-screen', + test = 'test' } export enum OriginPageEnum { From 521c58b9de05ef4b6e759b973b62cba0e45cc4a8 Mon Sep 17 00:00:00 2001 From: Tudor Date: Thu, 17 Jul 2025 17:53:39 +0300 Subject: [PATCH 04/38] revert --- tests/utils/enums.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/utils/enums.ts b/tests/utils/enums.ts index b261e430..141b303f 100644 --- a/tests/utils/enums.ts +++ b/tests/utils/enums.ts @@ -90,8 +90,7 @@ export enum GlobalSelectorEnum { closeButton = 'closeButton', signButton = 'signButton', transactionLink = '.transaction-link', - closePoupBtn = 'mvx-provider-idle-screen', - test = 'test' + closePoupBtn = 'mvx-provider-idle-screen' } export enum OriginPageEnum { From a0d7a618b4e8260e2b408dfceebcfba20319d667 Mon Sep 17 00:00:00 2001 From: Iulia Cimpeanu Date: Fri, 18 Jul 2025 12:25:06 +0300 Subject: [PATCH 05/38] Disabled in memory provider --- src/initConfig.ts | 21 +++++------ yarn.lock | 95 +++++++++++++++++++++++++---------------------- 2 files changed, 61 insertions(+), 55 deletions(-) diff --git a/src/initConfig.ts b/src/initConfig.ts index 7b13c53f..bed6c582 100644 --- a/src/initConfig.ts +++ b/src/initConfig.ts @@ -1,21 +1,20 @@ import './styles/globals.css'; import { walletConnectV2ProjectId } from 'config'; -import { EnvironmentsEnum, ICustomProvider, InitAppType } from './lib'; -import { InMemoryProvider } from './provider/inMemoryProvider'; +import { EnvironmentsEnum, InitAppType } from './lib'; -const providers: ICustomProvider[] = [ - { - name: 'In Memory Provider', - type: 'inMemoryProvider', - iconUrl: `${window.location.origin}/multiversx-white.svg`, - constructor: async (options) => new InMemoryProvider(options) - } -]; +// const providers: ICustomProvider[] = [ +// { +// name: 'In Memory Provider', +// type: 'inMemoryProvider', +// iconUrl: `${window.location.origin}/multiversx-white.svg`, +// constructor: async (options) => new InMemoryProvider(options) +// } +// ]; (window as any).multiversx = {}; // Option 1: Add providers using the `window.providers` array -(window as any).multiversx.providers = providers; +// (window as any).multiversx.providers = providers; export const config: InitAppType = { storage: { getStorageCallback: () => sessionStorage }, diff --git a/yarn.lock b/yarn.lock index 8cf0a042..feccfea2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -324,6 +324,11 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@emotion/unitless@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" + integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== + "@esbuild/android-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" @@ -515,7 +520,7 @@ dependencies: "@fortawesome/fontawesome-common-types" "6.5.1" -"@fortawesome/fontawesome-svg-core@>= 6.7.2": +"@fortawesome/fontawesome-svg-core@>=6.7.2": version "6.7.2" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz#0ac6013724d5cc327c1eb81335b91300a4fce2f2" integrity sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA== @@ -529,7 +534,7 @@ dependencies: "@fortawesome/fontawesome-common-types" "6.5.1" -"@fortawesome/free-solid-svg-icons@>= 6.7.2": +"@fortawesome/free-solid-svg-icons@>=6.7.2": version "6.7.2" resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz#fe25883b5eb8464a82918599950d283c465b57f6" integrity sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA== @@ -926,7 +931,7 @@ dependencies: "@types/node-fetch" "^2.5.10" -"@lit/react@^1.0.4": +"@lit/react@^1.0.7": version "1.0.8" resolved "https://registry.yarnpkg.com/@lit/react/-/react-1.0.8.tgz#b3e229173b7b57d550909bf95d8f3da1a9510557" integrity sha512-p2+YcF+JE67SRX3mMlJ1TKCSTsgyOVdAwd/nxp3NuV1+Cb6MWALbN6nT7Ld4tpmYofcE5kcaSY1YBB9erY+6fw== @@ -1059,19 +1064,19 @@ axios "^1.10.0" bip39 "3.1.0" -"@multiversx/sdk-dapp-ui@>=0.0.14", "@multiversx/sdk-dapp-ui@^0.x": - version "0.0.14" - resolved "https://registry.yarnpkg.com/@multiversx/sdk-dapp-ui/-/sdk-dapp-ui-0.0.14.tgz#66c01f15d64f0e2c87e95d54c0b3830da7f99ea7" - integrity sha512-dC2IprJrHFwlOmN3+Uj7eYwJr1cob5lrgx+XfmMWWBjIK9IYOx5jjzD+u51mDrsP7CyZP5LMJtNrz0qakbWpgQ== +"@multiversx/sdk-dapp-ui@>=0.0.16", "@multiversx/sdk-dapp-ui@^0.x": + version "0.0.16" + resolved "https://registry.yarnpkg.com/@multiversx/sdk-dapp-ui/-/sdk-dapp-ui-0.0.16.tgz#8a84b8eedec9de7abfeb3b338a02f1d53abcf674" + integrity sha512-yh4DMS7ZzZJvj3ElZqdlriT+ulkRtmrldOkpw5gM9OmNoknZZAbVRV1rjr83AOPdqM50u9rcJAxKanvPQfaYQQ== dependencies: - "@fortawesome/fontawesome-svg-core" ">= 6.7.2" - "@fortawesome/free-solid-svg-icons" ">= 6.7.2" + "@fortawesome/fontawesome-svg-core" ">=6.7.2" + "@fortawesome/free-solid-svg-icons" ">=6.7.2" "@rollup/plugin-image" "^3.0.3" - "@stencil/react-output-target" "0.8.2" - classnames ">= 2.5.1" + "@stencil/react-output-target" "1.2.0" + classnames ">=2.5.1" lodash.inrange "^3.3.0" lodash.range "^3.2.0" - qrcode ">= 1.5.4" + qrcode ">=1.5.4" sass-embedded "^1.85.1" "@multiversx/sdk-dapp-utils@2.0.2": @@ -1080,9 +1085,9 @@ integrity sha512-qxkYV3qtLxY6nckXsxtYZJ8zLY0zalpxgLQfYWaz2uOjEswraky/2PLxSaws1ts0YP3PPc0SrKIDfN9wVOA7EQ== "@multiversx/sdk-dapp@^5.x": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@multiversx/sdk-dapp/-/sdk-dapp-5.0.2.tgz#5ce55768d9152d04e89e6f3d21e64aba7f1aae2d" - integrity sha512-fCNt8tKtF2qIahUB8zXxJ2pfQomwi+rnIg+4rGxEP0PvRhKrN1jNEUsBL7/m+dsbPJvvLenkarAaKL/y0EbQgQ== + version "5.0.5" + resolved "https://registry.yarnpkg.com/@multiversx/sdk-dapp/-/sdk-dapp-5.0.5.tgz#99a7f57e1e437c512c5e713802ae1008aa2d1665" + integrity sha512-Z2t60pi7EY4yQ8JFsCNKIAq9sfJd4+VxLjVBr4Iq/nqu0UVwv3vxu4Yvda1XIzBRcs2QFKfwF3BiCqI/9IfJxg== dependencies: "@lifeomic/axios-fetch" "3.0.1" "@multiversx/sdk-extension-provider" "5.0.0" @@ -1107,7 +1112,7 @@ socket.io-client "4.7.5" zustand "4.4.7" optionalDependencies: - "@multiversx/sdk-dapp-ui" ">=0.0.14" + "@multiversx/sdk-dapp-ui" ">=0.0.16" "@multiversx/sdk-extension-provider@5.0.0": version "5.0.0" @@ -1523,14 +1528,14 @@ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== -"@stencil/react-output-target@0.8.2": - version "0.8.2" - resolved "https://registry.yarnpkg.com/@stencil/react-output-target/-/react-output-target-0.8.2.tgz#3503a192d6751085b4f9f89006faed4c17bc7849" - integrity sha512-O7zRCfRbiPmxaW3oaPBB3RFOMQOuy1dfkcUUg+6en6NckrRzC2YEAzzo6iIkppDrPW34TJJRy/mqJUdlBPLJ1g== +"@stencil/react-output-target@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@stencil/react-output-target/-/react-output-target-1.2.0.tgz#a5c48c182d7efcce4559786a5f06ae1caef81922" + integrity sha512-xDNpWdRg897T3Diy5V2d8dZUdjhc4QJ/5JZdTVyv3/e9UICdJPfCY6eKp/dWWgYlJ9AUE6GLHOI1iePZmLY12A== dependencies: - "@lit/react" "^1.0.4" - html-react-parser "^5.1.10" - style-object-to-css-string "^1.0.0" + "@lit/react" "^1.0.7" + html-react-parser "^5.2.2" + react-style-stringify "^1.2.0" ts-morph "^22.0.0" "@svgr/babel-plugin-add-jsx-attribute@8.0.0": @@ -3650,7 +3655,7 @@ classnames@2.3.2: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== -"classnames@>= 2.5.1": +classnames@>=2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== @@ -5715,15 +5720,15 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -html-react-parser@^5.1.10: - version "5.2.5" - resolved "https://registry.yarnpkg.com/html-react-parser/-/html-react-parser-5.2.5.tgz#4a0d62c129d5d5c63cc49f986c946552d737190c" - integrity sha512-bRPdv8KTqG9CEQPMNGksDqmbiRfVQeOidry8pVetdh/1jQ1Edx4KX5m0lWvDD89Pt4CqTYjK1BLz6NoNVxN/Uw== +html-react-parser@^5.2.2: + version "5.2.6" + resolved "https://registry.yarnpkg.com/html-react-parser/-/html-react-parser-5.2.6.tgz#547a8820be5c76bc2a10f0bb32dd9119d440438b" + integrity sha512-qcpPWLaSvqXi+TndiHbCa+z8qt0tVzjMwFGFBAa41ggC+ZA5BHaMIeMJla9g3VSp4SmiZb9qyQbmbpHYpIfPOg== dependencies: domhandler "5.0.3" html-dom-parser "5.1.1" react-property "2.0.2" - style-to-js "1.1.16" + style-to-js "1.1.17" htmlparser2@10.0.0: version "10.0.0" @@ -8332,7 +8337,7 @@ pure-rand@^6.0.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== -"qrcode@>= 1.5.4": +qrcode@>=1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88" integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg== @@ -8470,6 +8475,13 @@ react-router@6.16.0: dependencies: "@remix-run/router" "1.9.0" +react-style-stringify@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/react-style-stringify/-/react-style-stringify-1.2.0.tgz#d6ffc475dde8e9f8e3014f383f942666e8fb983a" + integrity sha512-88JZckqgbfXJaGcDQoTFKRmBwHBF0Ddaxz3PL9Q+vywAJruBY+NdN+ZiKSBe7r/pWtjbt0naZdtH5oNI1X3FLA== + dependencies: + "@emotion/unitless" "^0.10.0" + react@18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -9443,22 +9455,17 @@ strnum@^1.1.1: resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.1.2.tgz#57bca4fbaa6f271081715dbc9ed7cee5493e28e4" integrity sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA== -style-object-to-css-string@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/style-object-to-css-string/-/style-object-to-css-string-1.1.3.tgz#9427c3387c701a7c5860de4027f11c8fbec55caf" - integrity sha512-bISQoUsir/qGfo7vY8rw00ia9nnyE1jvYt3zZ2jhdkcXZ6dAEi74inMzQ6On57vFI+I4Fck6wOv5UI9BEwJDgw== - -style-to-js@1.1.16: - version "1.1.16" - resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.16.tgz#e6bd6cd29e250bcf8fa5e6591d07ced7575dbe7a" - integrity sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw== +style-to-js@1.1.17: + version "1.1.17" + resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.17.tgz#488b1558a8c1fd05352943f088cc3ce376813d83" + integrity sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA== dependencies: - style-to-object "1.0.8" + style-to-object "1.0.9" -style-to-object@1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.8.tgz#67a29bca47eaa587db18118d68f9d95955e81292" - integrity sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g== +style-to-object@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.9.tgz#35c65b713f4a6dba22d3d0c61435f965423653f0" + integrity sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw== dependencies: inline-style-parser "0.2.4" From b100c763dc2a3de2e5caa28cc62f0b0191cfe0af Mon Sep 17 00:00:00 2001 From: Tudor Morar Date: Fri, 18 Jul 2025 12:30:48 +0300 Subject: [PATCH 06/38] Update initConfig.ts --- src/initConfig.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/initConfig.ts b/src/initConfig.ts index bed6c582..015ab701 100644 --- a/src/initConfig.ts +++ b/src/initConfig.ts @@ -3,18 +3,25 @@ import './styles/globals.css'; import { walletConnectV2ProjectId } from 'config'; import { EnvironmentsEnum, InitAppType } from './lib'; -// const providers: ICustomProvider[] = [ -// { -// name: 'In Memory Provider', -// type: 'inMemoryProvider', -// iconUrl: `${window.location.origin}/multiversx-white.svg`, -// constructor: async (options) => new InMemoryProvider(options) -// } -// ]; +/* +// Enable this block to showcase a custom provider implementation + +import { EnvironmentsEnum, ICustomProvider, InitAppType } from './lib'; +import { InMemoryProvider } from './provider/inMemoryProvider'; + +const providers: ICustomProvider[] = [ + { + name: 'In Memory Provider', + type: 'inMemoryProvider', + iconUrl: `${window.location.origin}/multiversx-white.svg`, + constructor: async (options) => new InMemoryProvider(options) + } +]; (window as any).multiversx = {}; // Option 1: Add providers using the `window.providers` array -// (window as any).multiversx.providers = providers; +(window as any).multiversx.providers = providers; +*/ export const config: InitAppType = { storage: { getStorageCallback: () => sessionStorage }, From 6c195808d337c34ed76cdad149fc64b2b42436e6 Mon Sep 17 00:00:00 2001 From: Tudor Date: Fri, 18 Jul 2025 12:48:50 +0300 Subject: [PATCH 07/38] remove test file --- test.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test.txt diff --git a/test.txt b/test.txt deleted file mode 100644 index 9daeafb9..00000000 --- a/test.txt +++ /dev/null @@ -1 +0,0 @@ -test From d85050c5000c3dcfadfca90da420d870e8c0de45 Mon Sep 17 00:00:00 2001 From: Iulia Cimpeanu <72752718+iuliacimpeanu@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:56:33 +0300 Subject: [PATCH 08/38] Added GitHub button (#359) * Added GitHub button --- package.json | 1 + src/components/Layout/Header/Header.tsx | 2 + .../Layout/Header/components/GitHubButton.tsx | 17 +++++++ src/localConstants/routes/routeNames.enums.ts | 2 + yarn.lock | 47 +++++++++++-------- 5 files changed, 49 insertions(+), 20 deletions(-) create mode 100644 src/components/Layout/Header/components/GitHubButton.tsx diff --git a/package.json b/package.json index a8fcc109..7a573465 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "repository": "@multiversx/mx-template-dapp", "dependencies": { "@fortawesome/fontawesome-svg-core": "6.5.1", + "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "6.5.1", "@fortawesome/react-fontawesome": "0.2.0", "@multiversx/sdk-core": "14.2.6", diff --git a/src/components/Layout/Header/Header.tsx b/src/components/Layout/Header/Header.tsx index 29eaabc7..88684e78 100644 --- a/src/components/Layout/Header/Header.tsx +++ b/src/components/Layout/Header/Header.tsx @@ -4,6 +4,7 @@ import { environment } from 'config'; import { getAccountProvider, useGetIsLoggedIn } from 'lib'; import { RouteNamesEnum } from 'localConstants'; import MultiversXLogo from '../../../assets/img/multiversx-logo.svg?react'; +import { GitHubButton } from './components/GitHubButton'; import { NotificationsButton } from './components/NotificationsButton'; export const Header = () => { @@ -34,6 +35,7 @@ export const Header = () => { {isLoggedIn && ( <> + - ); -}; + ...props +}: ComponentProps) => ( + + {children} + +); diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx index 2d098ffb..1ece53a7 100644 --- a/src/components/Card/Card.tsx +++ b/src/components/Card/Card.tsx @@ -1,36 +1,42 @@ import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { PropsWithChildren } from 'react'; + import { WithClassnameType } from 'types'; -interface CardType extends PropsWithChildren, WithClassnameType { +// prettier-ignore +const styles = { + cardContainer: 'card-container flex flex-col gap-4 flex-1 rounded-xl bg-primary transition-all duration-200 ease-out p-6 lg:p-10 justify-center border border-secondary', + cardTitle: 'card-title flex justify-between items-center text-2xl font-medium group text-primary transition-all duration-200 ease-out', + cardRef: 'card-ref text-link hover:text-primary transition-all duration-200 ease-out flex items-center', + cardRefIcon: 'max-w-3.5 max-h-3.5', + cardDescription: 'card-description text-secondary transition-all duration-200 ease-out mb-6 text-lg font-medium' +} satisfies Record; + +interface CardPropsType extends PropsWithChildren, WithClassnameType { title: string; description?: string; reference: string; anchor?: string; } -export const Card = (props: CardType) => { - const { title, children, description, reference, anchor } = props; +export const Card = ({ + title, + children, + description, + reference, + anchor, + 'data-testid': dataTestId +}: CardPropsType) => ( +
+

+ {title} + + + +

- return ( -
-

- {title} - - - -

- {description &&

{description}

} - {children} -
- ); -}; + {description &&

{description}

} + {children} +
+); diff --git a/src/components/ContractAddress/ContractAddress.tsx b/src/components/ContractAddress/ContractAddress.tsx deleted file mode 100644 index 0f15fe76..00000000 --- a/src/components/ContractAddress/ContractAddress.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Label } from 'components'; -import { contractAddress } from 'config'; -import { - ACCOUNTS_ENDPOINT, - ExplorerLink, - getExplorerLink, - useGetNetworkConfig -} from 'lib'; - -export const ContractAddress = () => { - const { network } = useGetNetworkConfig(); - const explorerAddress = network.explorerAddress; - const explorerLink = getExplorerLink({ - to: `/${ACCOUNTS_ENDPOINT}/${contractAddress}`, - explorerAddress - }); - return ( -

- - - - {contractAddress} - -

- ); -}; diff --git a/src/components/ContractAddress/index.ts b/src/components/ContractAddress/index.ts deleted file mode 100644 index d01c3095..00000000 --- a/src/components/ContractAddress/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ContractAddress'; diff --git a/src/components/Drawer/Drawer.tsx b/src/components/Drawer/Drawer.tsx new file mode 100644 index 00000000..351062f4 --- /dev/null +++ b/src/components/Drawer/Drawer.tsx @@ -0,0 +1,74 @@ +import classNames from 'classnames'; +import { Fragment, MouseEvent, PropsWithChildren, ReactNode } from 'react'; +import { Sheet } from 'react-modal-sheet'; + +import { WithClassnameType } from 'types'; + +// prettier-ignore +const styles = { + drawer: 'drawer flex', + drawerBackdrop: 'drawer-backdrop bg-primary! transition-opacity duration-200 fixed inset-0 opacity-0 pointer-events-none z-40 ease-in-out md:hidden', + drawerBackdropVisible: 'opacity-90 pointer-events-auto!', + drawerContainer: 'drawer-container rounded-t-3xl! bg-primary! p-6 overflow-hidden border border-secondary before:absolute before:top-3 before:left-1/2 before:w-32 before:h-1 before:bg-secondary before:transform before:-translate-x-1/2', + drawerContentWrapper: 'drawer-content-wrapper flex-col flex', + drawerContentHeader: 'drawer-content-header py-2 mb-8 flex justify-between items-center', + drawerContentHeaderTitle: 'drawer-content-header-title font-medium text-2xl text-primary leading-none', + drawerContentHeaderClose: 'drawer-content-header-close opacity-50 cursor-pointer w-5 h-5 relative before:absolute before:top-1/2 before:left-1/2 before:w-full before:h-0.5 before:bg-tertiary before:transform before:-translate-x-1/2 before:-translate-y-1/2 before:rotate-45 after:absolute after:top-1/2 after:left-1/2 after:w-full after:h-0.5 after:bg-tertiary after:transform after:-translate-x-1/2 after:-translate-y-1/2 after:-rotate-45', + drawerContent: 'drawer-content' +} satisfies Record; + +interface DrawerPropsType extends PropsWithChildren, WithClassnameType { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + title: ReactNode; +} + +export const Drawer = ({ + isOpen, + setIsOpen, + children, + title, + className +}: DrawerPropsType) => { + const handleDismiss = (event: MouseEvent) => { + event.preventDefault(); + setIsOpen(false); + }; + + return ( + +
+ + setIsOpen(false)} + className={classNames(styles.drawer, className)} + dragVelocityThreshold={200} + dragCloseThreshold={0.3} + > + + +
+
+
{title}
+ +
+
+ +
{children}
+
+ + + + + ); +}; diff --git a/src/components/Drawer/index.ts b/src/components/Drawer/index.ts new file mode 100644 index 00000000..0529d646 --- /dev/null +++ b/src/components/Drawer/index.ts @@ -0,0 +1 @@ +export * from './Drawer'; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 00000000..bbccba0e --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,43 @@ +import { faHeart } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import moment from 'moment'; + +import { useGetNetworkConfig } from 'lib'; + +import { version } from '../../../package.json'; + +// prettier-ignore +const styles = { + footer: 'footer mx-auto w-full max-w-prose py-4 text-center', + footerContainer: 'footer-container flex flex-col gap-2 font-medium items-center justify-center text-sm text-[#989898]', + footerDescription: 'footer-description flex items-center justify-center gap-1 text-sm text-neutral-500 gap-1', + footerDescriptionNetwork: 'footer-description-network capitalize', + footerHeartIcon: 'text-red-500' +} satisfies Record; + +export const Footer = () => { + const { network } = useGetNetworkConfig(); + const currentYear = moment().year(); + + return ( +
+
+
+ + {network.id} Build + + + {version} +
+ +
+ Made with + + + + by the MultiversX team, {currentYear} +
+
+
+ ); +}; diff --git a/src/components/Layout/Footer/index.ts b/src/components/Footer/index.ts similarity index 100% rename from src/components/Layout/Footer/index.ts rename to src/components/Footer/index.ts diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 00000000..a95c6858 --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,198 @@ +import { faGithub } from '@fortawesome/free-brands-svg-icons'; +import { + faBell, + faCreditCard, + faPowerOff, + IconDefinition +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { MouseEvent } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { Logo, Tooltip } from 'components'; +import { GITHUB_REPO_URL } from 'config'; +import { + ACCOUNTS_ENDPOINT, + getAccountProvider, + MvxButton, + MvxDataWithExplorerLink, + NotificationsFeedManager, + useGetAccountInfo, + useGetIsLoggedIn, + useGetNetworkConfig +} from 'lib'; +import { RouteNamesEnum } from 'localConstants'; + +import { ThemeTooltip } from './components'; + +// prettier-ignore +const styles = { + header: 'header flex items-center justify-between px-4 h-16 md:h-20 md:px-10', + headerLogo: 'header-logo cursor-pointer transition-opacity duration-200 hover:opacity-75', + headerNavigation: 'header-navigation flex items-center gap-2 lg:gap-4', + headerNavigationButtons: 'header-navigation-buttons flex gap-2 lg:gap-4', + headerNavigationButton: 'header-navigation-button flex justify-center items-center w-8 lg:w-10 h-8 lg:h-10 rounded-xl cursor-pointer relative after:rounded-xl after:absolute after:bg-btn-variant hover:after:bg-btn-hover after:transition-all after:duration-200 after:ease-out after:left-0 after:right-0 after:top-0 after:bottom-0 after:pointer-events-none hover:after:opacity-100', + headerNavigationButtonIcon: 'header-navigation-button-icon flex justify-center relative text-xs lg:text-base z-1 items-center text-tertiary', + headerNavigationTooltip: 'header-navigation-tooltip p-1 leading-none whitespace-nowrap text-tertiary', + headerNavigationNetwork: 'header-navigation-network h-8 border border-secondary rounded-xl lg:h-10 relative w-22 flex items-center justify-center leading-none capitalize text-tertiary before:absolute before:rounded-full before:w-2 before:lg:w-2.5 before:h-2 before:lg:h-2.5 before:bg-btn-primary before:z-2 before:-top-0.25 before:lg:-top-0.5 before:-left-0.25 before:lg:-left-0.5 after:absolute after:bg-primary after:rounded-lg after:opacity-40 after:left-0 after:right-0 after:top-0 after:bottom-0 after:pointer-events-none', + headerNavigationNetworkLabel: 'header-navigation-network-label relative z-1', + headerNavigationConnectDesktop: 'header-navigation-connect-desktop h-8 lg:h-10 hidden sm:block!', + headerNavigationConnectMobile: 'header-navigation-connect-mobile w-8 h-8 bg-btn-tertiary cursor-pointer flex justify-center items-center sm:hidden text-xs rounded-xl', + headerNavigationConnectIcon: 'header-navigation-connect-icon text-accent', + headerNavigationAddress: 'header-navigation-address h-8 lg:h-10 w-8 lg:w-full text-primary justify-center text-xs rounded-xl lg:text-base lg:pr-4 lg:pl-5 max-w-100 flex relative lg:border lg:border-secondary lg:rounded-full items-center gap-3 after:absolute after:bg-btn-tertiary after:rounded-xl lg:after:rounded-full after:opacity-40 after:left-0 after:right-0 after:top-0 after:bottom-0 after:pointer-events-none', + headerNavigationAddressWallet: 'header-navigation-address-wallet relative z-1 text-accent hidden lg:flex!', + headerNavigationAddressExplorer: 'header-navigation-address-explorer min-w-0 relative z-1 hidden lg:block!', + headerNavigationAddressLogout: 'header-navigation-address-logout text-tertiary cursor-pointer relative z-1 transition-all duration-200 ease-out hover:text-accent', +} satisfies Record; + +interface HeaderBrowseButtonType { + handleClick: (event: MouseEvent) => void; + icon: IconDefinition; + isVisible: boolean; + label: string; +} + +export const Header = () => { + const { network } = useGetNetworkConfig(); + const { address } = useGetAccountInfo(); + + const isLoggedIn = useGetIsLoggedIn(); + const provider = getAccountProvider(); + const navigate = useNavigate(); + + const handleLogout = async (event: MouseEvent) => { + event.preventDefault(); + await provider.logout(); + navigate(RouteNamesEnum.home); + }; + + const handleGitHubBrowsing = (event: MouseEvent) => { + event.preventDefault(); + window.open(GITHUB_REPO_URL); + }; + + const handleLogIn = (event: MouseEvent) => { + event.preventDefault(); + navigate(RouteNamesEnum.unlock); + }; + + const handleNotificationsBrowsing = (event: MouseEvent) => { + event.preventDefault(); + NotificationsFeedManager.getInstance().openNotificationsFeed(); + }; + + const headerBrowseButtons: HeaderBrowseButtonType[] = [ + { + label: 'GitHub', + handleClick: handleGitHubBrowsing, + icon: faGithub as IconDefinition, + isVisible: true + }, + { + label: 'Notifications', + handleClick: handleNotificationsBrowsing, + icon: faBell, + isVisible: isLoggedIn + } + ]; + + const handleLogoClick = (event: MouseEvent) => { + event.preventDefault(); + navigate(isLoggedIn ? RouteNamesEnum.dashboard : RouteNamesEnum.home); + }; + + return ( +
+
+ +
+ + +
+ ); +}; diff --git a/src/components/Header/components/ThemeTooltip/ThemeTooltip.tsx b/src/components/Header/components/ThemeTooltip/ThemeTooltip.tsx new file mode 100644 index 00000000..18251cd9 --- /dev/null +++ b/src/components/Header/components/ThemeTooltip/ThemeTooltip.tsx @@ -0,0 +1,128 @@ +import { + faArrowRightLong, + faChevronDown +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import classNames from 'classnames'; + +import { Tooltip } from 'components/Tooltip/Tooltip'; +import { + useHandleThemeManagement, + ThemeOptionType +} from 'hooks/useHandleThemeManagement'; + +import { ThemeTooltipDots } from './components'; + +interface ThemeTooltipOptionType extends ThemeOptionType { + dotColors: string[]; +} + +// prettier-ignore +const styles = { + themeTooltip: 'theme-tooltip', + themeTooltipTrigger: 'theme-tooltip-trigger flex h-8 lg:h-10 cursor-pointer gap-1 lg:gap-2 items-center justify-center w-12 min-w-12 max-w-12 lg:min-w-16 lg:max-w-16 lg:w-16 relative after:absolute after:opacity-40 after:bg-btn-tertiary after:left-0 after:right-0 after:top-0 after:bottom-0 after:pointer-events-none after:rounded-xl after:duration-200 after:ease-out after:transition-all hover:after:opacity-100', + themeTooltipTriggerToggled: 'after:opacity-100', + themeTooltipTriggerDots: 'theme-tooltip-trigger-dots relative z-1', + themeTooltipTriggerIcon: 'theme-tooltip-trigger-icon transition-all text-xs relative z-1 duration-200 ease-out text-tertiary', + themeTooltipTriggerIconRotated: 'rotate-180', + themeTooltipOptionsWrapper: 'theme-tooltip-options-wrapper flex-col gap-1 flex md:-m-1', + themeTooltipOptionsTitle: 'theme-tooltip-options-title md:hidden leading-none mb-3 text-secondary', + themeTooltipOptions: 'theme-tooltip-options rounded-lg overflow-hidden flex-col flex gap-0.5 md:gap-1', + themeTooltipOption: 'theme-tooltip-option h-15 md:h-auto w-full md:w-60 flex p-3 flex group text-primary font-medium gap-3 items-center transition-all duration-200 ease-out cursor-pointer relative after:transition-all after:duration-200 after:ease-out after:absolute md:after:opacity-0 hover:after:opacity-40 after:left-0 after:right-0 after:bottom-0 after:top-0 after:bg-secondary md:after:bg-accent after:pointer-events-none md:after:rounded-lg', + themeTooltipOptionActive: 'after:bg-accent md:after:opacity-100 md:hover:after:opacity-100 !text-accent', + themeTooltipOptionDots: 'theme-tooltip-option-dots flex w-10 h-10 p-2 md:p-0 md:w-4 md:h-4 justify-between relative z-1', + themeTooltipOptionLabel: 'theme-tooltip-option-label leading-none relative z-1 text-base', + themeTooltipOptionArrow: 'theme-tooltip-option-arrow ml-auto duration-200 transition-all ease-out opacity-0 text-link group-hover:opacity-100' +} satisfies Record; + +export const ThemeTooltip = () => { + const { allThemeOptions, activeTheme, handleThemeSwitch } = + useHandleThemeManagement(); + + const themeDotColors: Record = { + 'mvx:dark-theme': ['#23F7DD', '#262626', '#B6B3AF', '#FFFFFF'], + 'mvx:vibe-theme': ['#471150', '#5A2A62', '#D200FA', '#FFFFFF'], + 'mvx:light-theme': ['#000000', '#A5A5A5', '#E2DEDC', '#F3EFED'] + }; + + const tooltipThemeOptions = allThemeOptions.map( + (option): ThemeTooltipOptionType => ({ + ...option, + dotColors: themeDotColors[option.identifier] + }) + ); + + const activeTooltipTheme = activeTheme + ? { ...activeTheme, dotColors: themeDotColors[activeTheme.identifier] } + : null; + + if (!activeTooltipTheme) { + return null; + } + + return ( + +
Themes
+ +
+ {tooltipThemeOptions.map((themeOption) => ( +
+ + +
+ {themeOption.label} +
+ + {themeOption.identifier !== activeTooltipTheme.identifier && ( + + )} +
+ ))} +
+
+ } + > + {(isDrawerOrTooltipOpen) => ( +
+ + + +
+ )} + + ); +}; diff --git a/src/components/Header/components/ThemeTooltip/components/ThemeTooltipDots/ThemeTooltipDots.tsx b/src/components/Header/components/ThemeTooltip/components/ThemeTooltipDots/ThemeTooltipDots.tsx new file mode 100644 index 00000000..7275f74e --- /dev/null +++ b/src/components/Header/components/ThemeTooltip/components/ThemeTooltipDots/ThemeTooltipDots.tsx @@ -0,0 +1,30 @@ +import classNames from 'classnames'; + +import { ThemeTooltipDotsPropsType } from './themeTooltipDots.types'; + +// prettier-ignore +const styles = { + themeTooltipDots: 'theme-tooltip-dots flex w-4 h-4 lg:w-5 gap-1 min-w-4 lg:min-w-5 lg:h-5 justify-between flex-wrap', + themeTooltipDot: 'theme-tooltip-dot rounded-full transition-all duration-200 ease-out w-1.5 h-1.5 min-w-1.5 lg:w-2 lg:h-2 lg:min-w-2', + themeTooltipMedium: 'theme-tooltip-medium', + themeTooltipLarge: 'theme-tooltip-large max-md:w-2.5 max-md:h-2.5 max-md:min-w-2.5' +} satisfies Record; + +export const ThemeTooltipDots = ({ + size = 'medium', + dotColors, + className +}: ThemeTooltipDotsPropsType) => ( +
+ {dotColors.map((dotColor, dotColorIndex) => ( +
+ ))} +
+); diff --git a/src/components/Header/components/ThemeTooltip/components/ThemeTooltipDots/index.ts b/src/components/Header/components/ThemeTooltip/components/ThemeTooltipDots/index.ts new file mode 100644 index 00000000..a0c78a41 --- /dev/null +++ b/src/components/Header/components/ThemeTooltip/components/ThemeTooltipDots/index.ts @@ -0,0 +1 @@ +export * from './ThemeTooltipDots'; diff --git a/src/components/Header/components/ThemeTooltip/components/ThemeTooltipDots/themeTooltipDots.types.ts b/src/components/Header/components/ThemeTooltip/components/ThemeTooltipDots/themeTooltipDots.types.ts new file mode 100644 index 00000000..e86ee2a4 --- /dev/null +++ b/src/components/Header/components/ThemeTooltip/components/ThemeTooltipDots/themeTooltipDots.types.ts @@ -0,0 +1,11 @@ +import { WithClassnameType } from 'types'; + +export interface ThemeTooltipDotsPropsType extends WithClassnameType { + dotColors: string[]; + size?: `${ThemeTooltipDotsSizeEnum}`; +} + +export enum ThemeTooltipDotsSizeEnum { + medium = 'medium', + large = 'large' +} diff --git a/src/components/Header/components/ThemeTooltip/components/index.ts b/src/components/Header/components/ThemeTooltip/components/index.ts new file mode 100644 index 00000000..a0c78a41 --- /dev/null +++ b/src/components/Header/components/ThemeTooltip/components/index.ts @@ -0,0 +1 @@ +export * from './ThemeTooltipDots'; diff --git a/src/components/Header/components/ThemeTooltip/index.ts b/src/components/Header/components/ThemeTooltip/index.ts new file mode 100644 index 00000000..3deaf1f3 --- /dev/null +++ b/src/components/Header/components/ThemeTooltip/index.ts @@ -0,0 +1 @@ +export * from './ThemeTooltip'; diff --git a/src/components/Header/components/index.ts b/src/components/Header/components/index.ts new file mode 100644 index 00000000..3deaf1f3 --- /dev/null +++ b/src/components/Header/components/index.ts @@ -0,0 +1 @@ +export * from './ThemeTooltip'; diff --git a/src/components/Layout/Header/index.ts b/src/components/Header/index.ts similarity index 100% rename from src/components/Layout/Header/index.ts rename to src/components/Header/index.ts diff --git a/src/components/Label/Label.tsx b/src/components/Label/Label.tsx index 18e971fe..24a7dd14 100644 --- a/src/components/Label/Label.tsx +++ b/src/components/Label/Label.tsx @@ -1,5 +1,10 @@ import { PropsWithChildren } from 'react'; -export const Label = ({ children }: PropsWithChildren) => { - return ; -}; +// prettier-ignore +const styles = { + labelContainer: 'label-container text-secondary transition-all duration-200 ease-out text-sm font-normal' +} satisfies Record; + +export const Label = ({ children }: PropsWithChildren) => ( + +); diff --git a/src/components/Layout/Footer/Footer.tsx b/src/components/Layout/Footer/Footer.tsx deleted file mode 100644 index 36a528fa..00000000 --- a/src/components/Layout/Footer/Footer.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import HeartIcon from 'assets/img/heart.svg?react'; - -export const Footer = () => { - return ( - - ); -}; diff --git a/src/components/Layout/Header/Header.tsx b/src/components/Layout/Header/Header.tsx deleted file mode 100644 index a789bdd8..00000000 --- a/src/components/Layout/Header/Header.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useNavigate } from 'react-router-dom'; -import { Button, MxLink } from 'components'; -import { environment } from 'config'; -import { getAccountProvider, useGetIsLoggedIn } from 'lib'; -import { RouteNamesEnum } from 'localConstants'; -import MultiversXLogo from '../../../assets/img/multiversx-logo.svg?react'; -import { GitHubButton } from './components/GitHubButton'; -import { NotificationsButton } from './components/NotificationsButton'; - -export const Header = () => { - const isLoggedIn = useGetIsLoggedIn(); - const navigate = useNavigate(); - const provider = getAccountProvider(); - - const handleLogout = async () => { - await provider.logout(); - navigate(RouteNamesEnum.home); - }; - - return ( -
- - - - - -
- ); -}; diff --git a/src/components/Layout/Header/components/GitHubButton.tsx b/src/components/Layout/Header/components/GitHubButton.tsx deleted file mode 100644 index af8727ed..00000000 --- a/src/components/Layout/Header/components/GitHubButton.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { faGithub } from '@fortawesome/free-brands-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { GITHUB_REPO_URL } from 'config'; - -export const GitHubButton = () => { - return ( - - - - ); -}; diff --git a/src/components/Layout/Header/components/NotificationsButton.tsx b/src/components/Layout/Header/components/NotificationsButton.tsx deleted file mode 100644 index 6f4a26b6..00000000 --- a/src/components/Layout/Header/components/NotificationsButton.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { faBell } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button } from 'components'; -import { NotificationsFeedManager } from 'lib'; - -export const NotificationsButton = () => { - const handleOpenNotificationsFeed = async () => { - await NotificationsFeedManager.getInstance().openNotificationsFeed(); - }; - - return ( - - ); -}; diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 976eb7b0..763540eb 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,16 +1,22 @@ import { PropsWithChildren } from 'react'; + +import { Footer, Header } from 'components'; import { AuthRedirectWrapper } from 'wrappers'; -import { Footer } from './Footer'; -import { Header } from './Header'; -export const Layout = ({ children }: PropsWithChildren) => { - return ( -
-
-
- {children} -
-
-
- ); -}; +// prettier-ignore +const styles = { + layoutContainer: 'layout-container flex min-h-screen flex-col bg-accent transition-all duration-200 ease-out', + mainContainer: 'main-container flex flex-grow items-stretch justify-center' +} satisfies Record; + +export const Layout = ({ children }: PropsWithChildren) => ( +
+
+ +
+ {children} +
+ +
+
+); diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx index c40a4641..784bc614 100644 --- a/src/components/Loader/Loader.tsx +++ b/src/components/Loader/Loader.tsx @@ -1,14 +1,16 @@ import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +// prettier-ignore +const styles = { + loaderContainer: 'loader-container flex justify-center items-center h-screen', + loaderIcon: 'loader-icon text-4xl text-primary transition-all duration-200 ease-out' +} satisfies Record; + export const Loader = () => { return ( -
- +
+
); }; diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx new file mode 100644 index 00000000..11ab7314 --- /dev/null +++ b/src/components/Logo/Logo.tsx @@ -0,0 +1,32 @@ +import classNames from 'classnames'; + +// prettier-ignore +const styles = { + logo: 'logo flex items-center justify-center gap-3', + logoIcon: 'logo-icon relative -bottom-0.75', + logoIconEmpty: 'logo-icon-empty w-4 h-4 bg-logo-secondary border-2 border-logo z-1 relative', + logoIconFilled: 'logo-icon-filled w-4 h-4 bg-logo-primary absolute left-0.75 bottom-0.75', + logoText: 'logo-text text-xl lg:text-2xl font-medium flex text-primary relative -top-0.5 leading-none', + logoTextHidden: 'logo-text-hidden hidden lg:!flex' +} satisfies Record; + +interface LogoPropsType { + hideTextOnMobile?: boolean; +} + +export const Logo = ({ hideTextOnMobile }: LogoPropsType) => ( +
+
+
+
+
+ +
+ dApp Template +
+
+); diff --git a/src/components/Logo/index.ts b/src/components/Logo/index.ts new file mode 100644 index 00000000..d97c6951 --- /dev/null +++ b/src/components/Logo/index.ts @@ -0,0 +1 @@ +export * from './Logo'; diff --git a/src/components/MissingNativeAuthError/MissingNativeAuthError.tsx b/src/components/MissingNativeAuthError/MissingNativeAuthError.tsx index 2aec7bd7..f634f2d6 100644 --- a/src/components/MissingNativeAuthError/MissingNativeAuthError.tsx +++ b/src/components/MissingNativeAuthError/MissingNativeAuthError.tsx @@ -1,19 +1,27 @@ import { OutputContainer } from '../OutputContainer'; +// prettier-ignore +const styles = { + missingNativeAuthError: 'missing-native-auth-error flex items-center gap-1', + missingNativeAuthErrorText: 'missing-native-auth-error-text', + missingNativeAuthErrorDanger: 'missing-native-auth-error-danger text-red-500 font-bold', + missingNativeAuthErrorItalic: 'missing-native-auth-error-italic font-bold italic leading-none text-primary transition-all duration-200 ease-out' +} satisfies Record; + export const MissingNativeAuthError = () => ( -
-

+

+ Information could - - NOT - + + + NOT + be displayed because - - nativeAuth - - is not active -

-
+ + + nativeAuth + is not active +

); diff --git a/src/components/MxLink/MxLink.tsx b/src/components/MxLink/MxLink.tsx deleted file mode 100644 index 43f630c5..00000000 --- a/src/components/MxLink/MxLink.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { PropsWithChildren } from 'react'; -import { Link } from 'react-router-dom'; -import { WithClassnameType } from 'types'; - -interface MxLinkPropsType extends PropsWithChildren, WithClassnameType { - to: string; -} - -export const MxLink = ({ - to, - children, - className = 'inline-block rounded-lg px-3 py-2 text-center hover:no-underline my-0 bg-blue-600 text-white hover:bg-blue-700 ml-2 mr-0' -}: MxLinkPropsType) => { - return ( - - {children} - - ); -}; diff --git a/src/components/MxLink/index.ts b/src/components/MxLink/index.ts deleted file mode 100644 index 7bdcb36a..00000000 --- a/src/components/MxLink/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './MxLink'; diff --git a/src/components/OutputContainer/OutputContainer.tsx b/src/components/OutputContainer/OutputContainer.tsx index 6381b1c3..8fcabd7a 100644 --- a/src/components/OutputContainer/OutputContainer.tsx +++ b/src/components/OutputContainer/OutputContainer.tsx @@ -1,8 +1,14 @@ import classNames from 'classnames'; import { PropsWithChildren } from 'react'; + import { Loader } from 'components'; import { WithClassnameType } from 'types'; +// prettier-ignore +const styles = { + outputContainer: 'output-container text-sm text-primary font-normal bg-secondary transition-all duration-300 rounded-xl' +} satisfies Record; + interface OutputContainerPropsType extends PropsWithChildren, WithClassnameType { @@ -17,10 +23,7 @@ export const OutputContainer = ({ }: OutputContainerPropsType) => (
{isLoading ? : children}
diff --git a/src/components/OutputContainer/components/PingPongOutput.tsx b/src/components/OutputContainer/components/PingPongOutput.tsx index a2c5ce9d..499b603c 100644 --- a/src/components/OutputContainer/components/PingPongOutput.tsx +++ b/src/components/OutputContainer/components/PingPongOutput.tsx @@ -1,7 +1,25 @@ -import { ContractAddress, Label } from 'components'; -import { SignedTransactionType } from 'lib'; +import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { Label } from 'components'; +import { contractAddress } from 'config'; +import { + ACCOUNTS_ENDPOINT, + getExplorerLink, + MvxCopyButton, + SignedTransactionType, + useGetNetworkConfig +} from 'lib'; + import { TransactionsOutput } from './TransactionsOutput'; +// prettier-ignore +const styles = { + pingPongAddressContainer: 'ping-pong-address-container flex gap-3 mb-4', + pingPongButtons: 'ping-pong-buttons flex gap-2', + timeRemaining: 'time-remaining text-red-600' +} satisfies Record; + type PingPongOutputType = { timeRemaining: string; pongAllowed: boolean; @@ -13,19 +31,39 @@ export const PingPongOutput = ({ pongAllowed, transactions }: PingPongOutputType) => { + const { network } = useGetNetworkConfig(); + if (!transactions || transactions?.length === 0) { return null; } + const explorerAddress = network.explorerAddress; + const explorerLink = getExplorerLink({ + to: `/${ACCOUNTS_ENDPOINT}/${contractAddress}`, + explorerAddress + }); + return ( <> - +
+ {contractAddress} + +
+ + + + + +
+
+ + {!pongAllowed && (

- {timeRemaining} until able to - pong + {timeRemaining} until + able to pong

)} diff --git a/src/components/OutputContainer/components/TransactionOutput.tsx b/src/components/OutputContainer/components/TransactionOutput.tsx index 8a0e4905..647cbdc3 100644 --- a/src/components/OutputContainer/components/TransactionOutput.tsx +++ b/src/components/OutputContainer/components/TransactionOutput.tsx @@ -1,15 +1,27 @@ +import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + import { Label } from 'components'; import { ACCOUNTS_ENDPOINT, - ExplorerLink, FormatAmount, getExplorerLink, + MvxCopyButton, SignedTransactionType, TRANSACTIONS_ENDPOINT, useGetAccountInfo, useGetNetworkConfig } from 'lib'; +// prettier-ignore +const styles = { + transactionContainer: 'transaction-container flex flex-col', + transactionElementContainer: 'transaction-elem-container flex gap-2', + transactionElement: 'transaction-elem flex gap-3 w-full', + buttons: 'buttons flex gap-2', + dataContainer: 'data-container whitespace-nowrap' +} satisfies Record; + export const TransactionOutput = ({ transaction }: { @@ -32,29 +44,41 @@ export const TransactionOutput = ({ }); return ( -
-

+

+

- + +

{transaction.hash} - + +
+ + + + + +
+

-

+ +

- +

{transaction.receiver} - + +
+ + + + + +
+

- +

@@ -64,7 +88,7 @@ export const TransactionOutput = ({ {transaction.gasLimit}

-

+

{decodedData}

diff --git a/src/components/OutputContainer/components/TransactionsOutput.tsx b/src/components/OutputContainer/components/TransactionsOutput.tsx index 03f576f3..885222e8 100644 --- a/src/components/OutputContainer/components/TransactionsOutput.tsx +++ b/src/components/OutputContainer/components/TransactionsOutput.tsx @@ -1,18 +1,22 @@ import { SignedTransactionType } from 'lib'; + import { TransactionOutput } from './TransactionOutput'; +// prettier-ignore +const styles = { + transactionsOutput: 'transactions-output flex flex-col gap-4' +} satisfies Record; + +interface TransactionsOutputPropsType { + transactions: SignedTransactionType[]; +} + export const TransactionsOutput = ({ transactions -}: { - transactions: SignedTransactionType[]; -}) => { - return ( -
- {transactions?.map((transaction) => { - return ( - - ); - })} -
- ); -}; +}: TransactionsOutputPropsType) => ( +
+ {transactions.map((transaction) => ( + + ))} +
+); diff --git a/src/components/PingPongComponent/PingPongComponent.tsx b/src/components/PingPongComponent/PingPongComponent.tsx new file mode 100644 index 00000000..9704fc27 --- /dev/null +++ b/src/components/PingPongComponent/PingPongComponent.tsx @@ -0,0 +1,185 @@ +import { useEffect, useState } from 'react'; +import { faArrowDown, faArrowUp } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { TokenLoginType } from '@multiversx/sdk-dapp/out/types/login.types'; +import moment from 'moment'; + +import { + Label, + MissingNativeAuthError, + OutputContainer, + PingPongOutput +} from 'components'; +import { contractAddress } from 'config'; +import { getCountdownSeconds, setTimeRemaining } from 'helpers'; +import { MvxButton, MvxDataWithExplorerLink } from 'lib'; +import { ACCOUNTS_ENDPOINT, Transaction, useGetPendingTransactions } from 'lib'; +import { ItemsIdentifiersEnum } from 'pages/Dashboard/dashboard.types'; + +// prettier-ignore +const styles = { + pingPongContainer: 'ping-pong-container flex flex-col gap-6', + infosContainer: 'infos-container flex flex-col gap-2', + addressComponent: 'address-component flex w-full justify-between', + timeRemaining: 'text-red-600', + buttonsContainer: 'buttons-container flex flex-col gap-2', + buttons: 'buttons flex justify-start gap-2', + buttonContent: 'button-content text-sm font-normal' +} satisfies Record; + +interface PingPongComponentPropsType { + identifier: `${ItemsIdentifiersEnum}`; + sendPingTransaction: (amount: any) => Promise; + sendPongTransaction: (transaction?: any) => Promise; + getTimeToPong: () => Promise; + pingAmount?: string; + getPingTransaction?: () => Promise; + getPongTransaction?: () => Promise; + tokenLogin?: TokenLoginType | null; +} + +export const PingPongComponent = ({ + identifier, + sendPingTransaction, + sendPongTransaction, + getTimeToPong, + pingAmount, + getPingTransaction, + getPongTransaction, + tokenLogin +}: PingPongComponentPropsType) => { + const transactions = useGetPendingTransactions(); + const hasPendingTransactions = transactions.length > 0; + + const [hasPing, setHasPing] = useState(true); + const [secondsLeft, setSecondsLeft] = useState(0); + + const setSecondsRemaining = async () => { + if (tokenLogin && !tokenLogin?.nativeAuthToken) { + return; + } + + const secondsRemaining = await getTimeToPong(); + const { canPing, timeRemaining } = setTimeRemaining(secondsRemaining); + + setHasPing(canPing); + if (timeRemaining && timeRemaining >= 0) { + setSecondsLeft(timeRemaining); + } + }; + + const onSendPingTransaction = async () => { + if (pingAmount) { + await sendPingTransaction(pingAmount); + } else if (getPingTransaction) { + const pingTransaction = await getPingTransaction(); + + if (!pingTransaction) { + return; + } + + await sendPingTransaction([pingTransaction]); + } + }; + + const onSendPongTransaction = async () => { + if (pingAmount) { + await sendPongTransaction(); + } else if (getPongTransaction) { + const pongTransaction = await getPongTransaction(); + + if (!pongTransaction) { + return; + } + + await sendPongTransaction([pongTransaction]); + } + }; + + const timeRemaining = moment() + .startOf('day') + .seconds(secondsLeft ?? 0) + .format('mm:ss'); + + const pongAllowed = secondsLeft === 0; + + useEffect(() => { + getCountdownSeconds({ secondsLeft, setSecondsLeft }); + }, [hasPing]); + + useEffect(() => { + setSecondsRemaining(); + }, [hasPendingTransactions]); + + if (tokenLogin && !tokenLogin?.nativeAuthToken) { + return ; + } + + return ( +
+
+ + + + {!hasPendingTransactions && ( + <> + + + {!pongAllowed && ( +

+ + {timeRemaining} + + until able to pong +

+ )} + + )} + + +
+
+ +
+
+ + + + Ping + + + + + + Pong + +
+
+
+ ); +}; diff --git a/src/components/PingPongComponent/index.ts b/src/components/PingPongComponent/index.ts new file mode 100644 index 00000000..7bf86b51 --- /dev/null +++ b/src/components/PingPongComponent/index.ts @@ -0,0 +1 @@ +export * from './PingPongComponent'; diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx new file mode 100644 index 00000000..aa1d3ff0 --- /dev/null +++ b/src/components/Tooltip/Tooltip.tsx @@ -0,0 +1,90 @@ +import classNames from 'classnames'; +import { CSSProperties, MouseEvent, useState } from 'react'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; + +import { Drawer } from 'components'; + +import { TooltipPlaceEnum, TooltipPropsType } from './tooltip.types'; + +// prettier-ignore +const styles = { + tooltipWrapper: 'tooltip-wrapper relative', + tooltipDrawer: 'tooltip-drawer md:hidden ', + tooltip: 'tooltip bg-primary! border! border-secondary! z-1 rounded-xl! p-0! visible! pointer-events-all! z-60! before:absolute before:inset-0 before:bg-primary before:rounded-xl before:z-1', + tooltipMobile: 'tooltip-mobile hidden md:flex!', + tooltipContent: 'tooltip-content p-2 leading-tight text-neutral-500 text-xs text-center z-2 relative', + tooltipArrow: 'tooltip-arrow bg-primary! border! border-secondary! w-2! h-2!', + tooltipTrigger: 'tooltip-trigger' +} satisfies Record; + +export const Tooltip = ({ + identifier, + children, + className, + content, + skipTooltip, + hasDrawer, + drawerTitle, + place = TooltipPlaceEnum.bottom, + ...props +}: TooltipPropsType) => { + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [isTooltipOpen, setIsTooltipOpen] = useState(false); + + const tooltipWrapperStyle = { + '--rt-transition-show-delay': '200ms', + '--rt-transition-closing-delay': '200ms', + '--rt-opacity': 1 + } as CSSProperties; + + const handleTriggerClick = (event: MouseEvent) => { + if (!hasDrawer) { + return; + } + + event.preventDefault(); + setIsDrawerOpen(true); + }; + + return ( +
+ {hasDrawer && ( + + {content} + + )} + + {!skipTooltip && ( + +
{content}
+
+ )} + +
+ {children(isDrawerOpen || isTooltipOpen)} +
+
+ ); +}; diff --git a/src/components/Tooltip/index.ts b/src/components/Tooltip/index.ts new file mode 100644 index 00000000..7594a8f0 --- /dev/null +++ b/src/components/Tooltip/index.ts @@ -0,0 +1 @@ +export * from './Tooltip'; diff --git a/src/components/Tooltip/tooltip.types.ts b/src/components/Tooltip/tooltip.types.ts new file mode 100644 index 00000000..1a1b4ea0 --- /dev/null +++ b/src/components/Tooltip/tooltip.types.ts @@ -0,0 +1,27 @@ +import { ReactNode } from 'react'; +import { ITooltip as ReactTooltipPropsType } from 'react-tooltip'; + +import { WithClassnameType } from 'types'; + +export enum TooltipPlaceEnum { + top = 'top', + left = 'left', + right = 'right', + bottom = 'bottom' +} + +export interface TooltipPropsType extends WithClassnameType { + children: (isTooltipOrDrawerOpen: boolean) => ReactNode; + openOnClick?: ReactTooltipPropsType['openOnClick']; + afterHide?: ReactTooltipPropsType['afterHide']; + afterShow?: ReactTooltipPropsType['afterShow']; + setIsOpen?: ReactTooltipPropsType['setIsOpen']; + clickable?: ReactTooltipPropsType['clickable']; + isOpen?: ReactTooltipPropsType['isOpen']; + identifier: string; + content: ReactNode; + skipTooltip?: boolean; + place?: `${TooltipPlaceEnum}`; + hasDrawer?: boolean; + drawerTitle?: ReactNode; +} diff --git a/src/components/TransactionsTable/TransactionsTable.tsx b/src/components/TransactionsTable/TransactionsTable.tsx index de1c5d50..101976bd 100644 --- a/src/components/TransactionsTable/TransactionsTable.tsx +++ b/src/components/TransactionsTable/TransactionsTable.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; + import { MvxTransactionsTable, ServerTransactionType, diff --git a/src/components/index.ts b/src/components/index.ts index de768795..ee7dab4a 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,10 +1,15 @@ +export * from './AddressComponent'; export * from './Button'; export * from './Card'; -export * from './ContractAddress'; +export * from './Drawer'; +export * from './Footer'; +export * from './Header'; export * from './Label'; export * from './Layout'; export * from './Loader'; +export * from './Logo'; export * from './MissingNativeAuthError'; -export * from './MxLink'; export * from './OutputContainer'; +export * from './PingPongComponent'; +export * from './Tooltip'; export * from './TransactionsTable'; diff --git a/src/config/config.mainnet.ts b/src/config/config.mainnet.ts index 7c7cfbce..db31719c 100644 --- a/src/config/config.mainnet.ts +++ b/src/config/config.mainnet.ts @@ -3,6 +3,8 @@ import { EnvironmentsEnum } from 'lib'; export * from './sharedConfig'; export const API_URL = 'https://template-api.multiversx.com'; +export const ID_API_URL = 'https://id-api.multiversx.com'; +export const USERS_API_URL = '/users/api/v1/users/'; export const contractAddress = 'erd1qqqqqqqqqqqqqpgqtmcuh307t6kky677ernjj9ulk64zq74w9l5qxyhdn7'; export const environment = EnvironmentsEnum.mainnet; diff --git a/src/config/sharedConfig.ts b/src/config/sharedConfig.ts index 0e0d89cc..a340f8c7 100644 --- a/src/config/sharedConfig.ts +++ b/src/config/sharedConfig.ts @@ -18,11 +18,8 @@ export const BATCH_TRANSACTIONS_SC = { }; export const GITHUB_REPO_URL = 'https://github.com/multiversx/mx-template-dapp'; - export const apiTimeout = 6000; - export const nativeAuth = true; - export const transactionSize = 10; // Generate your own WalletConnect 2 ProjectId here: https://cloud.walletconnect.com/app diff --git a/src/helpers/getDetectedBrowser.ts b/src/helpers/getDetectedBrowser.ts new file mode 100644 index 00000000..527f0bde --- /dev/null +++ b/src/helpers/getDetectedBrowser.ts @@ -0,0 +1,23 @@ +import { safeWindow } from '@multiversx/sdk-dapp'; + +import { BrowserEnum } from 'localConstants/browser.enum'; + +export const getDetectedBrowser = () => { + const nav = safeWindow?.navigator; + const userAgent = nav?.userAgent || ''; + + if (/Firefox/.test(userAgent)) { + return BrowserEnum.Firefox; + } + + if (nav && typeof (nav as any).brave !== 'undefined') { + return BrowserEnum.Brave; + } + if (userAgent.toLowerCase().includes('brave')) { + return BrowserEnum.Brave; + } + + if (/Chrome/.test(userAgent)) { + return BrowserEnum.Chrome; + } +}; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 4ba6c2eb..fb5a2a24 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,2 +1,2 @@ export * from './transactions'; -export * from './useWindowSize'; +export * from './useHandleThemeManagement'; diff --git a/src/hooks/transactions/useSendPingPongTransaction.ts b/src/hooks/transactions/useSendPingPongTransaction.ts index 50905c69..14b1e17b 100644 --- a/src/hooks/transactions/useSendPingPongTransaction.ts +++ b/src/hooks/transactions/useSendPingPongTransaction.ts @@ -1,4 +1,5 @@ import axios from 'axios'; + import { contractAddress } from 'config'; import { signAndSendTransactions } from 'helpers'; import { diff --git a/src/hooks/useHandleThemeManagement.ts b/src/hooks/useHandleThemeManagement.ts new file mode 100644 index 00000000..36d1d16f --- /dev/null +++ b/src/hooks/useHandleThemeManagement.ts @@ -0,0 +1,57 @@ +import { MouseEvent, useEffect, useState } from 'react'; + +import { ThemesEnum } from 'lib'; + +export interface ThemeOptionType { + identifier: `${ThemesEnum}` | 'mvx:vibe-theme'; + label: string; +} + +export const useHandleThemeManagement = () => { + const attributeHandle = 'data-mvx-theme'; + + const allThemeOptions: ThemeOptionType[] = [ + { identifier: 'mvx:dark-theme', label: 'TealLab' }, + { identifier: 'mvx:vibe-theme', label: 'VibeMode' }, + { identifier: 'mvx:light-theme', label: 'BrightLight' } + ]; + + const [rootTheme, setRootTheme] = useState( + document.documentElement.getAttribute(attributeHandle) + ); + + const activeTheme = allThemeOptions.find( + (themeOption) => themeOption.identifier === rootTheme + ); + + const handleThemeSwitch = + (themeOptionIdentifier: ThemeOptionType['identifier']) => + (event: MouseEvent) => { + event.preventDefault(); + setRootTheme(themeOptionIdentifier); + + document.documentElement.setAttribute( + attributeHandle, + themeOptionIdentifier + ); + }; + + useEffect(() => { + const observer = new MutationObserver(() => + setRootTheme(document.documentElement.getAttribute(attributeHandle)) + ); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: [attributeHandle] + }); + + return () => observer.disconnect(); + }, []); + + return { + activeTheme, + allThemeOptions, + handleThemeSwitch + }; +}; diff --git a/src/hooks/useWindowSize.ts b/src/hooks/useWindowSize.ts deleted file mode 100644 index 86adb471..00000000 --- a/src/hooks/useWindowSize.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useEffect, useState } from 'react'; - -export const useWindowSize = () => { - const [windowSize, setWindowSize] = useState<{ - width: number; - height: number; - }>({ - width: window.innerWidth, - height: window.innerHeight - }); - - useEffect(() => { - function handleResize() { - setWindowSize({ - width: window.innerWidth, - height: window.innerHeight - }); - } - window.addEventListener('resize', handleResize); - handleResize(); - - return () => window.removeEventListener('resize', handleResize); - }, []); - return windowSize; -}; diff --git a/src/index.tsx b/src/index.tsx index da6a66f7..db88c350 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,10 @@ -import React from 'react'; import ReactDOM from 'react-dom/client'; + import { initApp } from 'lib'; + import { App } from './App'; import { config } from './initConfig'; initApp(config).then(() => { - ReactDOM.createRoot(document.getElementById('root')!).render( - - - - ); + ReactDOM.createRoot(document.getElementById('root')!).render(); }); diff --git a/src/initConfig.ts b/src/initConfig.ts index 9e518d92..892666fb 100644 --- a/src/initConfig.ts +++ b/src/initConfig.ts @@ -2,6 +2,7 @@ import './styles/tailwind.css'; import './styles/style.css'; import { walletConnectV2ProjectId } from 'config'; + import { EnvironmentsEnum, InitAppType } from './lib'; /* @@ -29,6 +30,7 @@ export const config: InitAppType = { dAppConfig: { nativeAuth: true, environment: EnvironmentsEnum.devnet, + theme: 'mvx:dark-theme', providers: { walletConnect: { walletConnectV2ProjectId diff --git a/src/lib/sdkDapp/components/ExplorerLink/ExplorerLink.tsx b/src/lib/sdkDapp/components/ExplorerLink/ExplorerLink.tsx index cb725baf..7d948149 100644 --- a/src/lib/sdkDapp/components/ExplorerLink/ExplorerLink.tsx +++ b/src/lib/sdkDapp/components/ExplorerLink/ExplorerLink.tsx @@ -1,4 +1,5 @@ import { PropsWithChildren } from 'react'; + import { useGetNetworkConfig } from 'lib/sdkDapp/sdkDapp.hooks'; import { MvxExplorerLink } from 'lib/sdkDappUI/sdkDappUI.components'; import { WithClassnameType } from 'types/components.types'; diff --git a/src/lib/sdkDapp/components/FormatAmount/FormatAmount.tsx b/src/lib/sdkDapp/components/FormatAmount/FormatAmount.tsx index 6ea159a4..8bec579a 100644 --- a/src/lib/sdkDapp/components/FormatAmount/FormatAmount.tsx +++ b/src/lib/sdkDapp/components/FormatAmount/FormatAmount.tsx @@ -1,4 +1,5 @@ import { WithClassnameType } from 'types'; + import { MvxFormatAmount } from '../../../sdkDappUI/sdkDappUI.components'; import { MvxFormatAmountPropsType } from '../../../sdkDappUI/sdkDappUI.types'; import { DECIMALS, DIGITS } from '../../../sdkDappUtils'; diff --git a/src/lib/sdkDapp/sdkDapp.types.ts b/src/lib/sdkDapp/sdkDapp.types.ts index 1395fe76..762b76c4 100644 --- a/src/lib/sdkDapp/sdkDapp.types.ts +++ b/src/lib/sdkDapp/sdkDapp.types.ts @@ -7,3 +7,4 @@ export type { ServerTransactionType } from '@multiversx/sdk-dapp/out/types/serve export type { SignedTransactionType } from '@multiversx/sdk-dapp/out/types/transactions.types'; export type { TransactionsDisplayInfoType } from '@multiversx/sdk-dapp/out/types/transactions.types'; export type { TransactionsRowType } from '@multiversx/sdk-dapp/out/controllers/TransactionsTableController/transactionsTableController.types'; +export { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; diff --git a/src/lib/sdkDappUI/sdkDappUI.components.ts b/src/lib/sdkDappUI/sdkDappUI.components.ts index ec5d3e68..997d260a 100644 --- a/src/lib/sdkDappUI/sdkDappUI.components.ts +++ b/src/lib/sdkDappUI/sdkDappUI.components.ts @@ -1,7 +1,10 @@ export { + MvxButton, MvxCopyButton, + MvxDataWithExplorerLink, MvxExplorerLink, MvxFormatAmount, MvxTransactionsTable, + MvxTrim, MvxUnlockButton } from '@multiversx/sdk-dapp-ui/react'; diff --git a/src/lib/sdkDappUI/sdkDappUI.types.ts b/src/lib/sdkDappUI/sdkDappUI.types.ts index 64af950b..15782cce 100644 --- a/src/lib/sdkDappUI/sdkDappUI.types.ts +++ b/src/lib/sdkDappUI/sdkDappUI.types.ts @@ -1,3 +1,4 @@ export type { MvxCopyButton as MvxCopyButtonPropsType } from '@multiversx/sdk-dapp-ui/web-components/mvx-copy-button'; export type { MvxExplorerLink as MvxExplorerLinkPropsType } from '@multiversx/sdk-dapp-ui/web-components/mvx-explorer-link'; export type { MvxFormatAmount as MvxFormatAmountPropsType } from '@multiversx/sdk-dapp-ui/web-components/mvx-format-amount'; +export type { MvxTrim as MvxTrimPropsType } from '@multiversx/sdk-dapp-ui/web-components/mvx-trim'; diff --git a/src/localConstants/browser.enum.ts b/src/localConstants/browser.enum.ts new file mode 100644 index 00000000..c9d7a35b --- /dev/null +++ b/src/localConstants/browser.enum.ts @@ -0,0 +1,5 @@ +export enum BrowserEnum { + Chrome = 'Chrome', + Firefox = 'Firefox', + Brave = 'Brave' +} diff --git a/src/localConstants/dashboard/dashboardLinks.ts b/src/localConstants/dashboard/dashboardLinks.ts new file mode 100644 index 00000000..8ffb04c4 --- /dev/null +++ b/src/localConstants/dashboard/dashboardLinks.ts @@ -0,0 +1,8 @@ +export const DOCUMENTATION_LINK = + 'https://docs.multiversx.com/developers/tutorials/your-first-dapp/#set-up-the-dapp-template'; + +export const REACT_LINK = 'https://react.dev/'; +export const TYPESCRIPT_LINK = 'https://www.typescriptlang.org/'; + +export const SDK_DAPP_PACKAGE_LINK = + 'https://www.npmjs.com/package/@multiversx/sdk-dapp'; diff --git a/src/localConstants/dashboard/index.ts b/src/localConstants/dashboard/index.ts new file mode 100644 index 00000000..d23a16e2 --- /dev/null +++ b/src/localConstants/dashboard/index.ts @@ -0,0 +1 @@ +export * from './dashboardLinks'; diff --git a/src/localConstants/index.ts b/src/localConstants/index.ts index ace81441..d6780f15 100644 --- a/src/localConstants/index.ts +++ b/src/localConstants/index.ts @@ -1,3 +1,6 @@ +export * from './browser.enum'; +export * from './dashboard'; export * from './dataTestIds.enum'; +export * from './installExtensionsLinks'; export * from './routes'; export * from './signMessage'; diff --git a/src/localConstants/installExtensionsLinks.ts b/src/localConstants/installExtensionsLinks.ts new file mode 100644 index 00000000..f925c44c --- /dev/null +++ b/src/localConstants/installExtensionsLinks.ts @@ -0,0 +1,14 @@ +export const GET_LEDGER = 'https://www.ledger.com/'; +export const GET_XPORTAL = 'https://xportal.com/'; + +export const CHROME_EXTENSION_LINK = + 'https://chrome.google.com/webstore/detail/multiversx-defi-wallet/dngmlblcodfobpdpecaadgfbcggfjfnm'; + +export const CHROME_METAMASK_EXTENSION_LINK = + 'https://chromewebstore.google.com/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=en'; + +export const FIREFOX_ADDON_LINK = + 'https://addons.mozilla.org/en-US/firefox/addon/multiversx-defi-wallet'; + +export const FIREFOX_METAMASK_ADDON_LINK = + 'https://addons.mozilla.org/en-US/firefox/addon/ether-metamask'; diff --git a/src/localConstants/routes/routeNames.enums.ts b/src/localConstants/routes/routeNames.enums.ts index f728b432..6fa86d29 100644 --- a/src/localConstants/routes/routeNames.enums.ts +++ b/src/localConstants/routes/routeNames.enums.ts @@ -1,6 +1,5 @@ export enum RouteNamesEnum { home = '/', dashboard = '/dashboard', - unlock = '/unlock', - disclaimer = '/disclaimer' + unlock = '/unlock' } diff --git a/src/pages/Dashboard/Dashboard.tsx b/src/pages/Dashboard/Dashboard.tsx index 8bc779f9..c126ca58 100644 --- a/src/pages/Dashboard/Dashboard.tsx +++ b/src/pages/Dashboard/Dashboard.tsx @@ -1,8 +1,11 @@ +import classNames from 'classnames'; +import { useState } from 'react'; + import { contractAddress } from 'config'; import { WidgetType } from 'types/widget.types'; -import { Widget } from './components'; + +import { DashboardHeader, LeftPanel, Widget } from './components'; import { - Account, BatchTransactions, NativeAuth, PingPongAbi, @@ -12,13 +15,17 @@ import { Transactions } from './widgets'; -const WIDGETS: WidgetType[] = [ - { - title: 'Account', - widget: Account, - description: 'Connected account details', - reference: 'https://docs.multiversx.com/sdk-and-tools/sdk-dapp/#account' - }, +// prettier-ignore +const styles = { + dashboardContainer: 'dashboard-container flex w-screen overflow-hidden min-h-screen relative border-t border-b border-secondary transition-all duration-200', + mobilePanelContainer: 'mobile-panel-container fixed bottom-0 left-0 right-0 z-50 max-h-full overflow-y-auto lg:static lg:max-h-none lg:overflow-visible', + desktopPanelContainer: 'desktop-panel-container lg:flex', + dashboardContent: 'dashboard-content flex flex-col gap-6 justify-center items-center flex-1 w-full overflow-auto border-l border-secondary p-4 lg:p-6 transition-all duration-200 ease-out', + dashboardContentMobilePanelOpen: 'dashboard-content-mobile-panel-open opacity-20 lg:opacity-100 pointer-events-none', + dashboardWidgets: 'dashboard-widgets flex flex-col gap-6 w-full max-w-320' +} satisfies Record; + +const dashboardWidgets: WidgetType[] = [ { title: 'Ping & Pong (Manual)', widget: PingPongRaw, @@ -65,14 +72,14 @@ const WIDGETS: WidgetType[] = [ }, { title: 'Transactions (All)', - widget: Transactions, + widget: () => , description: 'List transactions for the connected account', reference: 'https://api.multiversx.com/#/accounts/AccountController_getAccountTransactions' }, { title: 'Transactions (Ping & Pong)', - widget: Transactions, + widget: () => , props: { receiver: contractAddress }, description: 'List transactions filtered for a given Smart Contract', reference: @@ -81,11 +88,36 @@ const WIDGETS: WidgetType[] = [ ]; export const Dashboard = () => { + const [isMobilePanelOpen, setIsMobilePanelOpen] = useState(false); + return ( -
- {WIDGETS.map((element) => ( - - ))} +
+
+ +
+ +
+ + +
+ {dashboardWidgets.map((element) => ( + + ))} +
+
); }; diff --git a/src/pages/Dashboard/components/DashboardHeader/DashboardHeader.tsx b/src/pages/Dashboard/components/DashboardHeader/DashboardHeader.tsx new file mode 100644 index 00000000..7666adc3 --- /dev/null +++ b/src/pages/Dashboard/components/DashboardHeader/DashboardHeader.tsx @@ -0,0 +1,49 @@ +import { + REACT_LINK, + SDK_DAPP_PACKAGE_LINK, + TYPESCRIPT_LINK +} from 'localConstants'; + +import { DashboardHeaderTextLink } from './components/DashboardHeaderTextLink'; + +// prettier-ignore +const styles = { + dashboardHeaderContainer: 'dashboard-header-container flex flex-col p-8 lg:p-10 justify-center items-center gap-6 self-stretch', + dashboardHeaderTitle: 'dashboard-header-title text-primary transition-all duration-300 text-center text-3xl xs:text-5xl lg:text-6xl font-medium', + dashboardHeaderDescription: 'dashboard-header-description text-secondary transition-all duration-300 text-center text-base xs:text-lg lg:text-xl font-medium', + dashboardHeaderDescriptionText: 'dashboard-header-description-text mx-1' +} satisfies Record; + +export const DashboardHeader = () => ( +
+
Welcome to dApp Template
+ +
+ + The MultiversX dApp Template, built using the + + + + React.js + + + and + + TypeScript + + + + technologies, is a basic implementation of + + + + @multiversx/sdk-dapp + + + + package, providing the basics for MultiversX authentication and TX + signing. + +
+
+); diff --git a/src/pages/Dashboard/components/DashboardHeader/components/DashboardHeaderTextLink/DashboardHeaderTextLink.tsx b/src/pages/Dashboard/components/DashboardHeader/components/DashboardHeaderTextLink/DashboardHeaderTextLink.tsx new file mode 100644 index 00000000..342755ea --- /dev/null +++ b/src/pages/Dashboard/components/DashboardHeader/components/DashboardHeaderTextLink/DashboardHeaderTextLink.tsx @@ -0,0 +1,24 @@ +import { PropsWithChildren } from 'react'; + +// prettier-ignore +const styles = { + linkAddress: 'dashboard-header-text-link underline hover:text-primary transition-all duration-200' +} satisfies Record; + +interface DashboardHeaderTextLinkPropsType extends PropsWithChildren { + linkAddress: string; +} + +export const DashboardHeaderTextLink = ({ + linkAddress, + children +}: DashboardHeaderTextLinkPropsType) => ( + + {children} + +); diff --git a/src/pages/Dashboard/components/DashboardHeader/components/DashboardHeaderTextLink/index.ts b/src/pages/Dashboard/components/DashboardHeader/components/DashboardHeaderTextLink/index.ts new file mode 100644 index 00000000..082b1cb4 --- /dev/null +++ b/src/pages/Dashboard/components/DashboardHeader/components/DashboardHeaderTextLink/index.ts @@ -0,0 +1 @@ +export * from './DashboardHeaderTextLink'; diff --git a/src/pages/Dashboard/components/DashboardHeader/index.ts b/src/pages/Dashboard/components/DashboardHeader/index.ts new file mode 100644 index 00000000..c4a59a52 --- /dev/null +++ b/src/pages/Dashboard/components/DashboardHeader/index.ts @@ -0,0 +1 @@ +export * from './DashboardHeader'; diff --git a/src/pages/Dashboard/components/LeftPanel/LeftPanel.tsx b/src/pages/Dashboard/components/LeftPanel/LeftPanel.tsx new file mode 100644 index 00000000..abf5068a --- /dev/null +++ b/src/pages/Dashboard/components/LeftPanel/LeftPanel.tsx @@ -0,0 +1,111 @@ +import { + faClose, + faPowerOff, + faWallet +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import classNames from 'classnames'; +import { useNavigate } from 'react-router-dom'; + +import { ReactComponent as IconExpand } from 'assets/img/expand-up-down.svg'; +import { AddressComponent, Logo } from 'components'; +import { getAccountProvider, useGetAccountInfo, useGetIsLoggedIn } from 'lib'; +import { RouteNamesEnum } from 'localConstants'; + +import { Account, SideMenu } from './components'; + +// prettier-ignore +const styles = { + leftPanelContainer: 'left-panel-container flex flex-col w-screen lg:w-80 gap-8 lg:gap-0 py-4 p-6 sticky lg:h-screen top-0 bg-primary lg:bg-accent transition-all duration-200 ease-out overflow-y-scroll', + leftPanelContainerOpen: 'left-panel-container-open rounded-t-2xl lg:rounded-t-none p-6', + leftPanelMobileHeader: 'left-panel-mobile-header flex lg:hidden justify-between items-center pt-2 pb-1 transition-all duration-200 ease-out', + leftPanelMobileHeaderIconClose: 'left-panel-mobile-header-icon-close text-link transition-all duration-200 ease-out', + leftPanelMobileHeaderIconOpen: 'left-panel-mobile-header-icon-open fill-primary transition-all duration-200 ease-out', + leftPanel: 'left-panel flex flex-col gap-4 lg:!block', + leftPanelHidden: 'hidden', + leftPanelMobileAddressSection: 'left-panel-mobile-address-section lg:hidden bg-accent transition-all duration-200 ease-out h-8 flex items-center justify-between rounded-full border border-secondary px-6 py-4', + leftPanelMobileAddress: 'left-panel-mobile-address flex gap-2 items-center justify-start w-[calc(100%-50px)]', + leftPanelMobileAddressIcon: 'left-panel-mobile-address-icon text-accent transition-all duration-200 ease-out', + logoutButton: 'text-center text-link hover:text-primary transition-all duration-200 ease-out cursor-pointer', + leftPanelComponents: 'flex flex-col gap-4 bg-accent p-6 lg:p-0 rounded-2xl transition-all duration-200 ease-out', + leftPanelBar: 'w-full h-0.25 bg-neutral-700 opacity-40 transition-all duration-200 ease-out' +} satisfies Record; + +interface LeftPanelPropsType { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; +} + +export const LeftPanel = ({ + isOpen = false, + setIsOpen +}: LeftPanelPropsType) => { + const handleOpenPanel = () => { + setIsOpen(!isOpen); + }; + + const { address } = useGetAccountInfo(); + + const isLoggedIn = useGetIsLoggedIn(); + + const provider = getAccountProvider(); + const navigate = useNavigate(); + + const handleLogout = async () => { + await provider.logout(); + navigate(RouteNamesEnum.home); + }; + + return ( +
+
+ + + {isOpen ? ( + + ) : ( + + )} +
+ +
+
+
+ + + +
+ + {isLoggedIn && ( + + )} +
+ +
+ + +
+ + +
+
+
+ ); +}; diff --git a/src/pages/Dashboard/components/LeftPanel/components/Account/Account.tsx b/src/pages/Dashboard/components/LeftPanel/components/Account/Account.tsx new file mode 100644 index 00000000..e321d8ae --- /dev/null +++ b/src/pages/Dashboard/components/LeftPanel/components/Account/Account.tsx @@ -0,0 +1,169 @@ +import { + faChevronUp, + faLayerGroup, + faWallet +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import classNames from 'classnames'; +import { ReactNode, useState } from 'react'; + +import { ReactComponent as XLogo } from 'assets/img/x-logo.svg'; +import { Label } from 'components'; +import { + DECIMALS, + DIGITS, + FormatAmountController, + MvxFormatAmount, + MvxTrim, + useGetAccountInfo, + useGetNetworkConfig +} from 'lib'; + +import { Username } from './components'; +import { useGetUserHerotag } from './hooks/useGetUserHerotag'; + +// prettier-ignore +const styles = { + connectedAccountContainer: 'connected-account flex flex-col gap-4', + connectedAccountHeader: 'connected-account-header flex justify-between items-center', + connectedAccountHeaderTitle: 'connected-account-header-title text-base transition-all duration-200 ease-out text-secondary', + connectedAccountHeaderIcon: 'connected-account-header-icon text-primary transition-transform duration-200 ease-out', + connectedAccountHeaderIconRotated: 'rotate-180', + connectedAccountDetails: 'connected-account-details flex flex-col', + connectedAccountDetailsHidden: 'hidden', + connectedAccountInfo: 'connected-account-info flex h-14 gap-2 items-center', + connectedAccountInfoIcon: 'connected-account-info-icon min-w-10 min-h-10 max-h-10 max-w-10 flex items-center justify-center text-tertiary border border-secondary rounded-lg overflow-hidden p-1.5 transition-all duration-200 ease-out', + connectedAccountInfoText: 'connected-account-info-text truncate flex flex-col', + connectedAccountInfoTextValue: 'connected-account-info-text-value text-primary transition-all duration-200 ease-out text-base', + connectedAccountDetailsIcon: 'connected-account-details-icon w-6 h-6', + connectedAccountDetailsHerotag: 'connected-account-details-herotag rounded-full', + connectedAccountDetailsXLogo: 'connected-account-details-xlogo fill-primary w-6 h-6 transition-all duration-200 ease-out', + connectedAccountDetailsTrimAddress: 'w-max' +} satisfies Record; + +interface AccountDetailsType { + icon: ReactNode | string; + label: string; + value: string | ReactNode; +} + +export const Account = () => { + const { network } = useGetNetworkConfig(); + const { address, account } = useGetAccountInfo(); + + const { isValid, valueDecimal, valueInteger, label } = + FormatAmountController.getData({ + digits: DIGITS, + decimals: DECIMALS, + egldLabel: network.egldLabel, + input: account.balance + }); + + const [isCollapsed, setIsCollapsed] = useState(false); + + const toggleCollapse = () => { + setIsCollapsed((isCollapsed) => !isCollapsed); + }; + + const [herotag, profileUrl] = useGetUserHerotag(address); + + const accountDetails: AccountDetailsType[] = [ + { + icon: ( + + ), + label: 'Address', + value: ( + + ) + }, + { + icon: herotag ? ( + profileUrl ? ( + + ) : ( + herotag.slice(0, 3) + ) + ) : ( + '@' + ), + label: 'Herotag', + value: + }, + { + icon: ( + + ), + label: 'Shard', + value: account.shard + }, + { + icon: , + label: 'Balance', + value: ( + + ) + } + ]; + + return ( +
+
+

+ Connected account details +

+ + +
+ +
+ {accountDetails.map((accountDetail, index) => ( +
+
+ {accountDetail.icon} +
+ +

+ + + {accountDetail.value} + +

+
+ ))} +
+
+ ); +}; diff --git a/src/pages/Dashboard/components/LeftPanel/components/Account/components/Username.tsx b/src/pages/Dashboard/components/LeftPanel/components/Account/components/Username.tsx new file mode 100644 index 00000000..5300d364 --- /dev/null +++ b/src/pages/Dashboard/components/LeftPanel/components/Account/components/Username.tsx @@ -0,0 +1,30 @@ +import { AccountType, trimUsernameDomain } from 'lib'; +import { DataTestIdsEnum } from 'localConstants'; +import { ProfileType } from 'types'; + +import { useGetUserHerotag } from '../hooks/useGetUserHerotag'; + +// prettier-ignore +const styles = { + usernameContainer: 'username-container flex gap-0.5', + herotag: 'herotag text-accent transition-all duration-200 ease-out' +} satisfies Record; + +export const Username = (props: { + account?: AccountType | ProfileType | null; + address: string; +}) => { + const { address } = props; + + const [herotag] = useGetUserHerotag(address); + + return ( +

+ {herotag ? '@' : ''} + + + {herotag ? trimUsernameDomain(herotag) : 'N/A'} + +

+ ); +}; diff --git a/src/pages/Dashboard/widgets/Account/components/index.ts b/src/pages/Dashboard/components/LeftPanel/components/Account/components/index.ts similarity index 100% rename from src/pages/Dashboard/widgets/Account/components/index.ts rename to src/pages/Dashboard/components/LeftPanel/components/Account/components/index.ts diff --git a/src/pages/Dashboard/components/LeftPanel/components/Account/hooks/useGetUserHerotag.tsx b/src/pages/Dashboard/components/LeftPanel/components/Account/hooks/useGetUserHerotag.tsx new file mode 100644 index 00000000..c36536b1 --- /dev/null +++ b/src/pages/Dashboard/components/LeftPanel/components/Account/hooks/useGetUserHerotag.tsx @@ -0,0 +1,39 @@ +import axios from 'axios'; +import { useEffect, useState } from 'react'; + +import { ID_API_URL, USERS_API_URL } from 'config/config.mainnet'; + +export const useGetUserHerotag = (address: string) => { + const [profileUrl, setProfileUrl] = useState(''); + const [herotag, setHerotag] = useState(''); + + const getUserProfileData = async (address?: string) => { + if (!address) { + return; + } + + try { + const { data } = await axios.get(`${USERS_API_URL}${address}`, { + baseURL: ID_API_URL + }); + + return data; + } catch (err) { + console.error('Unable to fetch profile url'); + } + }; + + useEffect(() => { + if (!address) return; + + const fetchUserProfileUrl = async () => { + const data = await getUserProfileData(address); + setProfileUrl(data?.profile?.url); + setHerotag(data?.herotag); + }; + + fetchUserProfileUrl(); + }, [address]); + + return [herotag, profileUrl]; +}; diff --git a/src/pages/Dashboard/widgets/Account/index.ts b/src/pages/Dashboard/components/LeftPanel/components/Account/index.ts similarity index 100% rename from src/pages/Dashboard/widgets/Account/index.ts rename to src/pages/Dashboard/components/LeftPanel/components/Account/index.ts diff --git a/src/pages/Dashboard/components/LeftPanel/components/SideMenu/SideMenu.tsx b/src/pages/Dashboard/components/LeftPanel/components/SideMenu/SideMenu.tsx new file mode 100644 index 00000000..cc2f5ed0 --- /dev/null +++ b/src/pages/Dashboard/components/LeftPanel/components/SideMenu/SideMenu.tsx @@ -0,0 +1,150 @@ +import { + faChevronUp, + faFilter, + faFingerprint, + faPenNib, + faRectangleList, + faTableTennisPaddleBall, + IconDefinition +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import classNames from 'classnames'; +import { FunctionComponent, SVGProps, useState } from 'react'; + +import { ReactComponent as IconBatch } from 'assets/img/batch-tx.svg'; +import { ReactComponent as IconAbi } from 'assets/img/ping-pong-abi.svg'; +import { ReactComponent as IconBackend } from 'assets/img/ping-pong-backend.svg'; +import { ItemsIdentifiersEnum } from 'pages/Dashboard/dashboard.types'; + +// prettier-ignore +const styles = { + sideMenuContainer: 'side-menu-container flex flex-col gap-4', + sideMenuHeader: 'side-menu-header flex items-center justify-between', + sideMenuHeaderTitle: 'side-menu-header-title text-base transition-all duration-200 ease-out text-secondary', + sideMenuHeaderIcon: 'side-menu-header-icon text-primary transition-transform duration-200 ease-out', + sideMenuHeaderIconRotated: 'rotate-180', + sideMenuItems: 'side-menu-items flex flex-col transition-all duration-200 ease-out', + sideMenuItemsHidden: 'hidden', + sideMenuItem: 'side-menu-item flex items-center gap-2 p-2 cursor-pointer text-tertiary hover:text-primary hover:bg-primary hover:font-bold transition-all duration-200 ease-out fill-tertiary hover:fill-primary hover:rounded-lg', + sideMenuItemActive: 'side-menu-item-active text-primary bg-primary fill-primary rounded-lg font-bold transition-all duration-200 ease-out', + sideMenuItemTitle: 'side-menu-item-title transition-all duration-200 ease-out text-sm' +} satisfies Record; + +interface SideMenuPropsType { + setIsOpen: (isOpen: boolean) => void; +} +interface MenuItemsType { + title: string; + icon?: IconDefinition | FunctionComponent>; + id: ItemsIdentifiersEnum; +} + +const menuItems: MenuItemsType[] = [ + { + title: 'Ping & Pong (Manual)', + icon: faTableTennisPaddleBall, + id: ItemsIdentifiersEnum.pingPongRaw + }, + { + title: 'Ping & Pong (ABI)', + icon: IconAbi, + id: ItemsIdentifiersEnum.pingPongAbi + }, + { + title: 'Ping & Pong (Backend)', + icon: IconBackend, + id: ItemsIdentifiersEnum.pingPongService + }, + { + title: 'Sign message', + icon: faPenNib, + id: ItemsIdentifiersEnum.signMessage + }, + { + title: 'Native auth', + icon: faFingerprint, + id: ItemsIdentifiersEnum.nativeAuth + }, + { + title: 'Batch Transactions', + icon: IconBatch, + id: ItemsIdentifiersEnum.batchTransactions + }, + { + title: 'Transactions (All)', + icon: faRectangleList, + id: ItemsIdentifiersEnum.transactionsAll + }, + { + title: 'Transactions (Ping & Pong)', + icon: faFilter, + id: ItemsIdentifiersEnum.transactionsPingPong + } +]; + +export const SideMenu = ({ setIsOpen }: SideMenuPropsType) => { + const [isCollapsed, setIsCollapsed] = useState(false); + const [activeItem, setActiveItem] = useState( + ItemsIdentifiersEnum.pingPongRaw + ); + + const toggleCollapse = () => { + setIsCollapsed((isCollapsed) => !isCollapsed); + }; + + const handleMenuItemClick = (id: ItemsIdentifiersEnum) => { + setIsOpen(false); + const target = document.getElementById(id); + if (target) { + const y = target.getBoundingClientRect().top + window.scrollY - 250; + window.scrollTo({ top: y, behavior: 'smooth' }); + + setActiveItem(id); + } + }; + + const setItemIcon = ( + icon: IconDefinition | FunctionComponent> + ) => { + if ('iconName' in icon) return ; + + const IconComponent = icon; + return ; + }; + + return ( +
+
+

Library

+ + +
+ +
+ {menuItems.map((item) => ( +
handleMenuItemClick(item.id)} + className={classNames(styles.sideMenuItem, { + [styles.sideMenuItemActive]: item.id === activeItem + })} + > + {item.icon && setItemIcon(item.icon)} + +
{item.title}
+
+ ))} +
+
+ ); +}; diff --git a/src/pages/Dashboard/components/LeftPanel/components/SideMenu/index.ts b/src/pages/Dashboard/components/LeftPanel/components/SideMenu/index.ts new file mode 100644 index 00000000..6b65bcfb --- /dev/null +++ b/src/pages/Dashboard/components/LeftPanel/components/SideMenu/index.ts @@ -0,0 +1 @@ +export * from './SideMenu'; diff --git a/src/pages/Dashboard/components/LeftPanel/components/index.ts b/src/pages/Dashboard/components/LeftPanel/components/index.ts new file mode 100644 index 00000000..3ba9126b --- /dev/null +++ b/src/pages/Dashboard/components/LeftPanel/components/index.ts @@ -0,0 +1,2 @@ +export * from './Account'; +export * from './SideMenu'; diff --git a/src/pages/Dashboard/components/LeftPanel/index.ts b/src/pages/Dashboard/components/LeftPanel/index.ts new file mode 100644 index 00000000..aedb0f9e --- /dev/null +++ b/src/pages/Dashboard/components/LeftPanel/index.ts @@ -0,0 +1 @@ +export * from './LeftPanel'; diff --git a/src/pages/Dashboard/components/Widget.tsx b/src/pages/Dashboard/components/Widget/Widget.tsx similarity index 100% rename from src/pages/Dashboard/components/Widget.tsx rename to src/pages/Dashboard/components/Widget/Widget.tsx diff --git a/src/pages/Dashboard/components/Widget/index.ts b/src/pages/Dashboard/components/Widget/index.ts new file mode 100644 index 00000000..b42adc88 --- /dev/null +++ b/src/pages/Dashboard/components/Widget/index.ts @@ -0,0 +1 @@ +export * from './Widget'; diff --git a/src/pages/Dashboard/components/index.ts b/src/pages/Dashboard/components/index.ts index b42adc88..ca9c4675 100644 --- a/src/pages/Dashboard/components/index.ts +++ b/src/pages/Dashboard/components/index.ts @@ -1 +1,3 @@ +export * from './DashboardHeader'; +export * from './LeftPanel'; export * from './Widget'; diff --git a/src/pages/Dashboard/dashboard.types.ts b/src/pages/Dashboard/dashboard.types.ts new file mode 100644 index 00000000..c7abf2e1 --- /dev/null +++ b/src/pages/Dashboard/dashboard.types.ts @@ -0,0 +1,10 @@ +export enum ItemsIdentifiersEnum { + pingPongRaw = 'ping-pong-raw', + pingPongAbi = 'ping-pong-abi', + pingPongService = 'ping-pong-service', + signMessage = 'sign-message', + nativeAuth = 'native-auth', + batchTransactions = 'batch-transactions', + transactionsAll = 'transactions-all', + transactionsPingPong = 'transactions-ping-pong' +} diff --git a/src/pages/Dashboard/widgets/Account/Account.tsx b/src/pages/Dashboard/widgets/Account/Account.tsx deleted file mode 100644 index b304693f..00000000 --- a/src/pages/Dashboard/widgets/Account/Account.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Label, OutputContainer } from 'components'; -import { FormatAmount, useGetAccountInfo } from 'lib'; -import { Username } from './components'; - -export const Account = () => { - const { address, account } = useGetAccountInfo(); - - return ( - -
-

- - {address} -

- - -

- {account.shard} -

- -

- - - -

-
-
- ); -}; diff --git a/src/pages/Dashboard/widgets/Account/components/Username.tsx b/src/pages/Dashboard/widgets/Account/components/Username.tsx deleted file mode 100644 index cdf1b48e..00000000 --- a/src/pages/Dashboard/widgets/Account/components/Username.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Label } from 'components'; -import { AccountType, trimUsernameDomain } from 'lib'; -import { DataTestIdsEnum } from 'localConstants'; -import { ProfileType } from 'types'; - -export const Username = (props: { - account: AccountType | ProfileType | null; -}) => { - const { account } = props; - - if (!account) { - return null; - } - - return ( -

- - - {account.username ? trimUsernameDomain(account.username) : 'N/A'} - -

- ); -}; diff --git a/src/pages/Dashboard/widgets/BatchTransactions/BatchTransactions.tsx b/src/pages/Dashboard/widgets/BatchTransactions/BatchTransactions.tsx index 0db48c91..9dc2dfff 100644 --- a/src/pages/Dashboard/widgets/BatchTransactions/BatchTransactions.tsx +++ b/src/pages/Dashboard/widgets/BatchTransactions/BatchTransactions.tsx @@ -1,20 +1,39 @@ import { faArrowsRotate, - faPaperPlane + faPaperPlane, + IconDefinition } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, OutputContainer, TransactionsOutput } from 'components'; + +import { MvxButton } from 'lib'; +import { OutputContainer, TransactionsOutput } from 'components'; import { useGetAccount, useGetNetworkConfig, useGetPendingTransactions } from 'lib'; +import { ItemsIdentifiersEnum } from 'pages/Dashboard/dashboard.types'; + import { sendBatchTransactions, signAndAutoSendBatchTransactions, swapAndLockTokens } from './helpers'; +// prettier-ignore +const styles = { + batchTx: 'batch-tx flex flex-col gap-6', + buttonsContainer: 'buttons-container flex flex-col md:flex-row gap-2 items-start', + batchTxButton: 'batch-tx-button text-sm font-normal' +} satisfies Record; + +interface BatchTransactionsButtonsType { + dataTestId: string; + onClickFunction: () => Promise; + icon: IconDefinition; + label: string; +} + export const BatchTransactions = () => { const { address, nonce } = useGetAccount(); const { network } = useGetNetworkConfig(); @@ -56,38 +75,51 @@ export const BatchTransactions = () => { }); }; - return ( -
-
- - - -
+ const batchTransactionsButtons: BatchTransactionsButtonsType[] = [ + { + dataTestId: 'sign-auto-send', + onClickFunction: executeSignAndAutoSendBatchTransactions, + icon: faPaperPlane, + label: 'Sign & send batch' + }, + { + dataTestId: 'send-transactions', + onClickFunction: executeBatchTransactions, + icon: faPaperPlane, + label: 'Sign batch & controlled sending' + }, + { + dataTestId: 'swap-lock', + onClickFunction: executeSwapAndLockTokens, + icon: faArrowsRotate, + label: 'Swap & Lock' + } + ]; + return ( +
+ +
+ {batchTransactionsButtons.map((button) => ( + + + + {button.label} + + ))} +
); }; diff --git a/src/pages/Dashboard/widgets/BatchTransactions/helpers/getBatchTransactions.ts b/src/pages/Dashboard/widgets/BatchTransactions/helpers/getBatchTransactions.ts index 9aa6e7cf..1e45f9c1 100644 --- a/src/pages/Dashboard/widgets/BatchTransactions/helpers/getBatchTransactions.ts +++ b/src/pages/Dashboard/widgets/BatchTransactions/helpers/getBatchTransactions.ts @@ -1,4 +1,5 @@ import BigNumber from 'bignumber.js'; + import { Address, Transaction, diff --git a/src/pages/Dashboard/widgets/BatchTransactions/helpers/sendBatchTransactions.ts b/src/pages/Dashboard/widgets/BatchTransactions/helpers/sendBatchTransactions.ts index 9f29cdde..c146acbe 100644 --- a/src/pages/Dashboard/widgets/BatchTransactions/helpers/sendBatchTransactions.ts +++ b/src/pages/Dashboard/widgets/BatchTransactions/helpers/sendBatchTransactions.ts @@ -1,5 +1,6 @@ import { getAccountProvider } from 'lib'; import { TransactionProps } from 'types'; + import { getBatchTransactions } from './getBatchTransactions'; import { sendAndTrackTransactions } from './sendAndTrackTransactions'; diff --git a/src/pages/Dashboard/widgets/BatchTransactions/helpers/signAndAutoSendBatchTransactions.ts b/src/pages/Dashboard/widgets/BatchTransactions/helpers/signAndAutoSendBatchTransactions.ts index 8140cb16..0b8beeda 100644 --- a/src/pages/Dashboard/widgets/BatchTransactions/helpers/signAndAutoSendBatchTransactions.ts +++ b/src/pages/Dashboard/widgets/BatchTransactions/helpers/signAndAutoSendBatchTransactions.ts @@ -1,5 +1,6 @@ import { getAccountProvider, TransactionsDisplayInfoType } from 'lib'; import { TransactionProps } from 'types'; + import { getBatchTransactions } from './getBatchTransactions'; import { sendAndTrackTransactions } from './sendAndTrackTransactions'; diff --git a/src/pages/Dashboard/widgets/BatchTransactions/helpers/swapAndLockTokens.ts b/src/pages/Dashboard/widgets/BatchTransactions/helpers/swapAndLockTokens.ts index 165a6527..a145d2c0 100644 --- a/src/pages/Dashboard/widgets/BatchTransactions/helpers/swapAndLockTokens.ts +++ b/src/pages/Dashboard/widgets/BatchTransactions/helpers/swapAndLockTokens.ts @@ -1,5 +1,6 @@ import { getAccountProvider, TransactionsDisplayInfoType } from 'lib'; import { TransactionProps } from 'types'; + import { getSwapAndLockTransactions } from './getSwapAndLockTransactions'; import { sendAndTrackTransactions } from './sendAndTrackTransactions'; diff --git a/src/pages/Dashboard/widgets/NativeAuth/NativeAuth.tsx b/src/pages/Dashboard/widgets/NativeAuth/NativeAuth.tsx index e8550f75..b4ff1b36 100644 --- a/src/pages/Dashboard/widgets/NativeAuth/NativeAuth.tsx +++ b/src/pages/Dashboard/widgets/NativeAuth/NativeAuth.tsx @@ -1,14 +1,47 @@ import { useEffect } from 'react'; + import { Label, MissingNativeAuthError, OutputContainer } from 'components'; -import { FormatAmount, useGetAccount, useGetLoginInfo } from 'lib'; -import { Username } from '../Account/components'; +import { + ACCOUNTS_ENDPOINT, + DECIMALS, + DIGITS, + FormatAmountController, + MvxDataWithExplorerLink, + MvxFormatAmount, + useGetAccount, + useGetLoginInfo, + useGetNetworkConfig +} from 'lib'; +import { Username } from 'pages/Dashboard/components/LeftPanel/components/Account/components'; +import { ItemsIdentifiersEnum } from 'pages/Dashboard/dashboard.types'; + import { useGetProfile } from './hooks'; +// prettier-ignore +const styles = { + nativeAuthContainer: 'native-auth-container flex flex-col gap-8', + nativeAuthAddressContainer: 'native-auth-address-container flex flex-col gap-2', + nativeAuthAddress: 'native-auth-address flex justify-between w-full', + nativeAuthDetails: 'native-auth-details flex gap-8 w-full', + nativeAuthDetailContainer: 'native-auth-detail-container flex flex-col gap-2 sm:w-1/3', + nativeAuthAmount: 'native-auth-amount flex gap-1', + nativeAuthMissingProfile: 'native-auth-missing-profile flex items-center gap-1' +} satisfies Record; + export const NativeAuth = () => { + const { network } = useGetNetworkConfig(); const { tokenLogin, isLoggedIn } = useGetLoginInfo(); const account = useGetAccount(); const { isLoading, profile, getProfile } = useGetProfile(); + const { isValid, valueDecimal, valueInteger, label } = + FormatAmountController.getData({ + digits: DIGITS, + decimals: DECIMALS, + egldLabel: network.egldLabel, + input: account.balance + }); + useEffect(() => { // On page refresh, tokenInfo is null which implies that we do not have access to loginInfo data if (isLoggedIn && tokenLogin?.nativeAuthToken) { @@ -24,7 +57,7 @@ export const NativeAuth = () => { if (!profile && !isLoading) { return ( -
+

Unable to load profile

@@ -32,20 +65,56 @@ export const NativeAuth = () => { } return ( - -

- {account.address ?? 'N/A'} -

- - -

- {account.shard ?? 'N/A'} -

- -
- - +
+
+ + + + + +
+ +
+
+ + + + + +
+ +
+ + + +

{profile?.shard ?? 'N/A'}

+
+
+ +
+ + + +
+ +
+
+
- +
); }; diff --git a/src/pages/Dashboard/widgets/NativeAuth/hooks/useGetProfile.ts b/src/pages/Dashboard/widgets/NativeAuth/hooks/useGetProfile.ts index 97c99839..1d02bc0b 100644 --- a/src/pages/Dashboard/widgets/NativeAuth/hooks/useGetProfile.ts +++ b/src/pages/Dashboard/widgets/NativeAuth/hooks/useGetProfile.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import { useState } from 'react'; + import { API_URL } from 'config'; import { ProfileType } from 'types'; diff --git a/src/pages/Dashboard/widgets/PingPongAbi/PingPongAbi.tsx b/src/pages/Dashboard/widgets/PingPongAbi/PingPongAbi.tsx index 50550898..64d3f994 100644 --- a/src/pages/Dashboard/widgets/PingPongAbi/PingPongAbi.tsx +++ b/src/pages/Dashboard/widgets/PingPongAbi/PingPongAbi.tsx @@ -1,112 +1,21 @@ -import { faArrowDown, faArrowUp } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import moment from 'moment'; -import { useEffect, useState } from 'react'; -import { - Button, - ContractAddress, - Label, - OutputContainer, - PingPongOutput -} from 'components'; -import { getCountdownSeconds, setTimeRemaining } from 'helpers'; +import { PingPongComponent } from 'components'; import { useSendPingPongTransaction } from 'hooks'; -import { useGetPendingTransactions } from 'lib'; + import { useGetPingAmount, useGetTimeToPong } from './hooks'; export const PingPongAbi = () => { - const transactions = useGetPendingTransactions(); - const hasPendingTransactions = transactions.length > 0; - const getTimeToPong = useGetTimeToPong(); const { sendPingTransactionFromAbi, sendPongTransactionFromAbi } = useSendPingPongTransaction(); const pingAmount = useGetPingAmount(); - const [hasPing, setHasPing] = useState(true); - const [secondsLeft, setSecondsLeft] = useState(0); - - const setSecondsRemaining = async () => { - const secondsRemaining = await getTimeToPong(); - const { canPing, timeRemaining } = setTimeRemaining(secondsRemaining); - - setHasPing(canPing); - if (timeRemaining && timeRemaining >= 0) { - setSecondsLeft(timeRemaining); - } - }; - - const onSendPingTransaction = async () => { - await sendPingTransactionFromAbi(pingAmount); - }; - - const onSendPongTransaction = async () => { - await sendPongTransactionFromAbi(); - }; - - const timeRemaining = moment() - .startOf('day') - .seconds(secondsLeft ?? 0) - .format('mm:ss'); - - const pongAllowed = secondsLeft === 0; - - useEffect(() => { - getCountdownSeconds({ secondsLeft, setSecondsLeft }); - }, [hasPing]); - - useEffect(() => { - setSecondsRemaining(); - }, [hasPendingTransactions]); - return ( -
-
-
- - - -
-
- - - {!hasPendingTransactions && ( - <> - - {!pongAllowed && ( -

- - {timeRemaining} until able - to pong -

- )} - - )} - - -
-
+ ); }; diff --git a/src/pages/Dashboard/widgets/PingPongAbi/hooks/useGetPingAmount.ts b/src/pages/Dashboard/widgets/PingPongAbi/hooks/useGetPingAmount.ts index 75300c1f..27f22e2e 100644 --- a/src/pages/Dashboard/widgets/PingPongAbi/hooks/useGetPingAmount.ts +++ b/src/pages/Dashboard/widgets/PingPongAbi/hooks/useGetPingAmount.ts @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; + import { contractAddress } from 'config'; import pingPongAbi from 'contracts/ping-pong.abi.json'; import { diff --git a/src/pages/Dashboard/widgets/PingPongRaw/PingPongRaw.tsx b/src/pages/Dashboard/widgets/PingPongRaw/PingPongRaw.tsx index f02b679f..77bd83cd 100644 --- a/src/pages/Dashboard/widgets/PingPongRaw/PingPongRaw.tsx +++ b/src/pages/Dashboard/widgets/PingPongRaw/PingPongRaw.tsx @@ -1,111 +1,23 @@ -import { faArrowDown, faArrowUp } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import moment from 'moment'; -import { useEffect, useState } from 'react'; -import { - Button, - ContractAddress, - Label, - OutputContainer, - PingPongOutput -} from 'components'; -import { getCountdownSeconds, setTimeRemaining } from 'helpers'; +import { PingPongComponent } from 'components'; import { useSendPingPongTransaction } from 'hooks'; -import { useGetPendingTransactions } from 'lib'; + import { useGetPingAmount, useGetTimeToPong } from './hooks'; // Raw transaction are being done by directly requesting to API instead of calling the smartcontract export const PingPongRaw = () => { + const pingAmount = useGetPingAmount(); const getTimeToPong = useGetTimeToPong(); const { sendPingTransaction, sendPongTransaction } = useSendPingPongTransaction(); - const transactions = useGetPendingTransactions(); - const hasPendingTransactions = transactions.length > 0; - const pingAmount = useGetPingAmount(); - - const [hasPing, setHasPing] = useState(true); - const [secondsLeft, setSecondsLeft] = useState(0); - - const setSecondsRemaining = async () => { - const secondsRemaining = await getTimeToPong(); - const { canPing, timeRemaining } = setTimeRemaining(secondsRemaining); - - setHasPing(canPing); - if (timeRemaining && timeRemaining >= 0) { - setSecondsLeft(timeRemaining); - } - }; - - const onSendPingTransaction = async () => { - await sendPingTransaction(pingAmount); - }; - - const onSendPongTransaction = async () => { - await sendPongTransaction(); - }; - - const timeRemaining = moment() - .startOf('day') - .seconds(secondsLeft ?? 0) - .format('mm:ss'); - - const pongAllowed = secondsLeft === 0; - - useEffect(() => { - getCountdownSeconds({ secondsLeft, setSecondsLeft }); - }, [hasPing]); - - useEffect(() => { - setSecondsRemaining(); - }, [hasPendingTransactions]); - return ( -
-
-
- - - -
-
- - - {!hasPendingTransactions && ( - <> - - {!pongAllowed && ( -

- - {timeRemaining} until able - to pong -

- )} - - )} - -
-
+ ); }; diff --git a/src/pages/Dashboard/widgets/PingPongRaw/hooks/useGetPingAmount.ts b/src/pages/Dashboard/widgets/PingPongRaw/hooks/useGetPingAmount.ts index e18867c8..80d19603 100644 --- a/src/pages/Dashboard/widgets/PingPongRaw/hooks/useGetPingAmount.ts +++ b/src/pages/Dashboard/widgets/PingPongRaw/hooks/useGetPingAmount.ts @@ -1,8 +1,10 @@ import axios from 'axios'; import BigNumber from 'bignumber.js'; import { useEffect, useState } from 'react'; + import { contractAddress } from 'config'; import { useGetNetworkConfig } from 'lib'; + import { PingPongResponseType } from '../types'; const decodeAmount = (data: PingPongResponseType) => { diff --git a/src/pages/Dashboard/widgets/PingPongRaw/hooks/useGetTimeToPong.ts b/src/pages/Dashboard/widgets/PingPongRaw/hooks/useGetTimeToPong.ts index d887dc48..6b065dba 100644 --- a/src/pages/Dashboard/widgets/PingPongRaw/hooks/useGetTimeToPong.ts +++ b/src/pages/Dashboard/widgets/PingPongRaw/hooks/useGetTimeToPong.ts @@ -1,7 +1,9 @@ import axios from 'axios'; import BigNumber from 'bignumber.js'; + import { contractAddress } from 'config'; import { Address, useGetAccount, useGetNetworkConfig } from 'lib'; + import { PingPongResponseType } from '../types'; const decodeTime = (data: PingPongResponseType) => { diff --git a/src/pages/Dashboard/widgets/PingPongService/PingPongService.tsx b/src/pages/Dashboard/widgets/PingPongService/PingPongService.tsx index 11be404c..b82de262 100644 --- a/src/pages/Dashboard/widgets/PingPongService/PingPongService.tsx +++ b/src/pages/Dashboard/widgets/PingPongService/PingPongService.tsx @@ -1,18 +1,7 @@ -import { faArrowDown, faArrowUp } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import moment from 'moment'; -import { useEffect, useState } from 'react'; -import { - Button, - ContractAddress, - Label, - MissingNativeAuthError, - OutputContainer, - PingPongOutput -} from 'components'; -import { getCountdownSeconds, setTimeRemaining } from 'helpers'; +import { PingPongComponent } from 'components'; import { useSendPingPongTransaction } from 'hooks'; -import { useGetLoginInfo, useGetPendingTransactions } from 'lib'; +import { useGetLoginInfo } from 'lib'; + import { useGetPingTransaction, useGetPongTransaction, @@ -21,117 +10,23 @@ import { // The transactions are being done by directly requesting to template-dapp service export const PingPongService = () => { - const [hasPing, setHasPing] = useState(true); - const [secondsLeft, setSecondsLeft] = useState(0); - - const { sendPingTransactionFromService, sendPongTransactionFromService } = - useSendPingPongTransaction(); const getTimeToPong = useGetTimeToPong(); const getPingTransaction = useGetPingTransaction(); const getPongTransaction = useGetPongTransaction(); - const transactions = useGetPendingTransactions(); - const hasPendingTransactions = transactions.length > 0; const { tokenLogin } = useGetLoginInfo(); - - const setSecondsRemaining = async () => { - if (!tokenLogin?.nativeAuthToken) { - return; - } - - const secondsRemaining = await getTimeToPong(); - const { canPing, timeRemaining } = setTimeRemaining(secondsRemaining); - - setHasPing(canPing); - if (timeRemaining && timeRemaining >= 0) { - setSecondsLeft(timeRemaining); - } - }; - - const onSendPingTransaction = async () => { - const pingTransaction = await getPingTransaction(); - - if (!pingTransaction) { - return; - } - - await sendPingTransactionFromService([pingTransaction]); - }; - - const onSendPongTransaction = async () => { - const pongTransaction = await getPongTransaction(); - - if (!pongTransaction) { - return; - } - - await sendPongTransactionFromService([pongTransaction]); - }; - - const timeRemaining = moment() - .startOf('day') - .seconds(secondsLeft ?? 0) - .format('mm:ss'); - - const pongAllowed = secondsLeft === 0; - - useEffect(() => { - getCountdownSeconds({ secondsLeft, setSecondsLeft }); - }, [hasPing]); - - useEffect(() => { - setSecondsRemaining(); - }, [hasPendingTransactions]); - - if (!tokenLogin?.nativeAuthToken) { - return ; - } + const { sendPingTransactionFromService, sendPongTransactionFromService } = + useSendPingPongTransaction(); return ( -
-
-
- - - -
-
- - - {!hasPendingTransactions && ( - <> - - {!pongAllowed && ( -

- - {timeRemaining} until able - to pong -

- )} - - )} - -
-
+ ); }; diff --git a/src/pages/Dashboard/widgets/PingPongService/hooks/tests/useGetPingTransaction.test.ts b/src/pages/Dashboard/widgets/PingPongService/hooks/tests/useGetPingTransaction.test.ts index 7f661410..dc6641bb 100644 --- a/src/pages/Dashboard/widgets/PingPongService/hooks/tests/useGetPingTransaction.test.ts +++ b/src/pages/Dashboard/widgets/PingPongService/hooks/tests/useGetPingTransaction.test.ts @@ -1,5 +1,6 @@ import { renderHook } from '@testing-library/react'; import axios from 'axios'; + import { useGetPingTransaction } from '../useGetPingTransaction'; const pingTransaction = { diff --git a/src/pages/Dashboard/widgets/PingPongService/hooks/tests/useGetPongTransaction.test.ts b/src/pages/Dashboard/widgets/PingPongService/hooks/tests/useGetPongTransaction.test.ts index c67fb7e2..fc41faec 100644 --- a/src/pages/Dashboard/widgets/PingPongService/hooks/tests/useGetPongTransaction.test.ts +++ b/src/pages/Dashboard/widgets/PingPongService/hooks/tests/useGetPongTransaction.test.ts @@ -1,5 +1,6 @@ import { renderHook } from '@testing-library/react'; import axios from 'axios'; + import { useGetPongTransaction } from '../useGetPongTransaction'; const pongTransaction = { diff --git a/src/pages/Dashboard/widgets/PingPongService/hooks/tests/useGetTimeToPong.test.ts b/src/pages/Dashboard/widgets/PingPongService/hooks/tests/useGetTimeToPong.test.ts index 63e34053..367cd535 100644 --- a/src/pages/Dashboard/widgets/PingPongService/hooks/tests/useGetTimeToPong.test.ts +++ b/src/pages/Dashboard/widgets/PingPongService/hooks/tests/useGetTimeToPong.test.ts @@ -1,5 +1,6 @@ import { renderHook } from '@testing-library/react'; import axios from 'axios'; + import { useGetTimeToPong } from '../useGetTimeToPong'; describe('useGetTimeToPong', () => { diff --git a/src/pages/Dashboard/widgets/PingPongService/hooks/useGetPingTransaction.ts b/src/pages/Dashboard/widgets/PingPongService/hooks/useGetPingTransaction.ts index 4b4c718c..1e34bf8c 100644 --- a/src/pages/Dashboard/widgets/PingPongService/hooks/useGetPingTransaction.ts +++ b/src/pages/Dashboard/widgets/PingPongService/hooks/useGetPingTransaction.ts @@ -1,4 +1,5 @@ import axios from 'axios'; + import { API_URL } from 'config'; import { Transaction } from 'lib'; diff --git a/src/pages/Dashboard/widgets/PingPongService/hooks/useGetPongTransaction.ts b/src/pages/Dashboard/widgets/PingPongService/hooks/useGetPongTransaction.ts index e93297f9..8ddf59e5 100644 --- a/src/pages/Dashboard/widgets/PingPongService/hooks/useGetPongTransaction.ts +++ b/src/pages/Dashboard/widgets/PingPongService/hooks/useGetPongTransaction.ts @@ -1,4 +1,5 @@ import axios from 'axios'; + import { API_URL } from 'config'; import { Transaction } from 'lib'; diff --git a/src/pages/Dashboard/widgets/PingPongService/hooks/useGetTimeToPong.ts b/src/pages/Dashboard/widgets/PingPongService/hooks/useGetTimeToPong.ts index e6d46ab1..cb4e8773 100644 --- a/src/pages/Dashboard/widgets/PingPongService/hooks/useGetTimeToPong.ts +++ b/src/pages/Dashboard/widgets/PingPongService/hooks/useGetTimeToPong.ts @@ -1,5 +1,7 @@ import axios from 'axios'; + import { API_URL } from 'config'; + import { TimeToPongResponseType } from '../types'; export const useGetTimeToPong = () => { diff --git a/src/pages/Dashboard/widgets/SignMessage/SignMessage.tsx b/src/pages/Dashboard/widgets/SignMessage/SignMessage.tsx index d8048220..9bf03ddf 100644 --- a/src/pages/Dashboard/widgets/SignMessage/SignMessage.tsx +++ b/src/pages/Dashboard/widgets/SignMessage/SignMessage.tsx @@ -1,14 +1,32 @@ import { faArrowsRotate, faBroom, - faFileSignature + faPaste, + faPenNib } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { MvxButton } from '@multiversx/sdk-dapp-ui/react'; import { MouseEvent, useState } from 'react'; -import { Button, OutputContainer } from 'components'; + +import { OutputContainer } from 'components'; import { Address, getAccountProvider, Message, useGetAccount } from 'lib'; +import { ItemsIdentifiersEnum } from 'pages/Dashboard/dashboard.types'; + import { SignFailure, SignSuccess } from './components'; +// prettier-ignore +const styles = { + signMessageContainer: 'sign-message-container flex flex-col gap-6', + signMessage: 'sign-message flex flex-col gap-2', + signMessageLabel: 'sign-message-label text-secondary transition-all duration-200 ease-out text-sm font-normal', + signMessageText: 'sign-message-text text-secondary transition-all duration-200 ease-out resize-none w-full h-32 rounded-lg focus:outline-none', + signMessagePasteButtonContainer: 'sign-message-paste-button-container w-full flex justify-end', + signMessagePasteButton: 'sign-message-paste-button text-tertiary text-sm font-semibold flex items-center bg-btn-tertiary rounded-md cursor-pointer px-1 transition-all duration-200 ease-out', + signMessagePasteButtonText: 'sign-message-paste-button-text p-1', + signMessageButton: 'sign-message-button flex gap-2 items-start', + signButtonContent: 'sign-button-content text-sm font-normal' +} satisfies Record; + export const SignMessage = () => { const [message, setMessage] = useState(''); const [signedMessage, setSignedMessage] = useState(null); @@ -51,56 +69,92 @@ export const SignMessage = () => { setState('pending'); }; + const handlePasteClick = async () => { + const message = await navigator.clipboard.readText(); + + setMessage(message); + }; + return ( -
-
+
+
+ + + {!['success', 'error'].includes(state) && ( +