Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion __mocks__/fixtures/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const mockWallet: Wallet = {
balance: 0,
user_id: mockUser.id,
token: "ICP",
address: "",
address: "0xd0 5AfA 87A5 99b8 AD8C ff71 b1eC 7129 e3Ed fe08 b9",
payouts: [mockPayout],
description: ""
}
24 changes: 24 additions & 0 deletions __tests__/components/cards/Wallet.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import CardsWallet from "@/components/cards/wallet";
import "@testing-library/jest-dom";
import { mockWallet } from "../../../__mocks__/fixtures/wallet";
import { screen } from "@testing-library/react";
import { renderWithRedux } from "@__mocks__/renderWithRedux";

// use the actual component when it is done tested
jest.mock("../../../src/components/sections/profile/modals/EditAddress/index.tsx", () => {
return <h1>hello</h1>;
});

jest.mock("@/hooks/useTypedDispatch.ts", () => ({
useDispatch: jest.fn(),
}));

describe("Wallet card component", () => {
it("renders the wallet component with all the children", () => {
renderWithRedux(<CardsWallet wallet={mockWallet} disabled={false} />);
expect(screen.getByTestId("cardWalletId")).toBeInTheDocument();
expect(screen.getByTestId("overviewId")).toBeInTheDocument();
expect(screen.getByTestId("cashoutAddressId")).toBeInTheDocument();
expect(screen.queryByText("profile.wallet.payout.text")).not.toBeNull();
});
});
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const config = {
"react-markdown": "<rootDir>/__mocks__/react-markdown.tsx",
[`^(${esModules})-.*`]: "<rootDir>/__mocks__/plugin.ts",
unified: "<rootDir>/__mocks__/unified.ts",
"^@/(.*)$": "<rootDir>/src/$1",
},
};

Expand Down
2 changes: 2 additions & 0 deletions jest.polyfills.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Object.defineProperties(globalThis, {

const { Blob, File } = require("node:buffer");
const { fetch, Headers, FormData, Request, Response } = require("undici");
const { clearImmediate } = require("node:timers");

Object.defineProperties(globalThis, {
fetch: { value: fetch, writable: true },
Expand All @@ -30,4 +31,5 @@ Object.defineProperties(globalThis, {
FormData: { value: FormData },
Request: { value: Request },
Response: { value: Response },
clearImmediate: { value: clearImmediate },
});
153 changes: 0 additions & 153 deletions src/components/cards/Wallet.tsx

This file was deleted.

96 changes: 96 additions & 0 deletions src/components/cards/wallet/CashoutAddress.tsx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you refactor this component in way that we have less of if else ? : ? : ? :

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't have any if else, we are using a ternary operator to avoid many if conditions

Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import ArrowButton from "@/components/ui/button/Arrow";
import { useTranslation } from "next-i18next";
import { useDispatch } from "@/hooks/useTypedDispatch";
import { setCurrentWallet } from "@/store/feature/user/wallets.slice";
import { toggleBodyScrolling } from "@/store/feature/ui.slice";
import { useSelector } from "@/hooks/useTypedSelector";
import { openVerificationModal } from "@/store/feature/kyc.slice";
import { Wallet } from "@/types/wallet";
import { useCallback, useMemo } from "react";

interface CashoutAddressProps {
wallet: Wallet;
setShowEditModal: (show: boolean) => void;
disabled: boolean;
setShowPayoutModal: (show: boolean) => void;
testId?:string
}

export default function CashoutAddress({ wallet, setShowEditModal, disabled, setShowPayoutModal, testId='cashoutAddressId' }: CashoutAddressProps) {
const { t } = useTranslation();
const dispatch = useDispatch();
const user = useSelector((state) => state.user.data);
const isKycVerified = user?.kycStatus === "VERIFIED";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you move this in a useMemo?

const address = useMemo(() => (wallet.address ? wallet.address.match(/.{1,4}/g) : null), [wallet.address]);
const cashable = useMemo(() => String(wallet.token).toUpperCase() !== "DAC", [wallet.token]);


const triggerEditAddress = useCallback(() => {
dispatch(setCurrentWallet(wallet));
setShowEditModal(true);
dispatch(toggleBodyScrolling(true));
}, [dispatch, setShowEditModal, wallet]);

const triggerCashout = useCallback(() => {
setShowPayoutModal(true);
dispatch(toggleBodyScrolling(true));
}, [setShowPayoutModal, dispatch]);

const triggerKYCVerification = useCallback(() => {
dispatch(
openVerificationModal({
description: t("kyc.payout.reason"),
completedActionText: t("kyc.payout.button.completed"),
completedAction: () => {
triggerCashout();
},
})
);
}, [dispatch, t, triggerCashout]);
const cashout = () => {
if (!isKycVerified) return triggerKYCVerification();
triggerCashout();
};
return (
<div className="px-7 pt-6 flex-1 pb-24 lg:pb-24" data-testId={testId}>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix typo in data-testid attribute.

The data-testid attribute should be written in lowercase to ensure consistency and avoid potential issues.

-  <div className="px-7 pt-6 flex-1 pb-24 lg:pb-24" data-testId={testId}>
+  <div className="px-7 pt-6 flex-1 pb-24 lg:pb-24" data-testid={testId}>
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="px-7 pt-6 flex-1 pb-24 lg:pb-24" data-testId={testId}>
<div className="px-7 pt-6 flex-1 pb-24 lg:pb-24" data-testid={testId}>

{cashable ? (
<div className="text-sm text-gray-700">
{address ? (
<p className="leading-5 text-sm flex gap-x-2 gap-y-1 flex-wrap font-mono font-normal">
{address.map((part, k) => (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{address.map((part, k) => (
{address.map((part, i) => (

<span key={`address-${k}`} className="mr-2">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<span key={`address-${k}`} className="mr-2">
<span key={`address-${i}`} className="mr-2">

{part}
</span>
))}
</p>
) : (
<p>{wallet.description}</p>
)}
<div className="text-gray-700 text-sm mt-3">
<span className="cursor-pointer hover:underline" onClick={triggerEditAddress}>
{address ? t("profile.wallets.address-change") : t("profile.wallets.address-set")}
</span>
</div>
</div>
) : (
<div className="prose">
<p
dangerouslySetInnerHTML={{
__html: t("profile.wallets.uncashable", {
token: `${wallet.title}`,
link: "https://discord.gg/5yDZvVnpQQ",
}),
}}
/>
</div>
Comment on lines +76 to +85
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using dangerouslySetInnerHTML.

Using dangerouslySetInnerHTML can expose users to cross-site scripting (XSS) attacks. Consider using a safer alternative to render the message.

-  <p
-    dangerouslySetInnerHTML={{
-      __html: t("profile.wallets.uncashable", {
-        token: `${wallet.title}`,
-        link: "https://discord.gg/5yDZvVnpQQ",
-      }),
-    }}
-  />
+  <p>
+    {t("profile.wallets.uncashable", {
+      token: `${wallet.title}`,
+      link: <a href="https://discord.gg/5yDZvVnpQQ">https://discord.gg/5yDZvVnpQQ</a>,
+    })}
+  </p>

Committable suggestion was skipped due to low confidence.

Tools
Biome

[error] 78-78: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

)}
{cashable && (
<div className="right-2 absolute bottom-2 mt-5">
<ArrowButton disabled={!wallet.balance || !wallet.address || disabled} variant="outline-primary" minWidthClass="min-w-40" onClick={cashout}>
{t("profile.wallets.cash-out")}
</ArrowButton>
</div>
)}
</div>
);
}
37 changes: 37 additions & 0 deletions src/components/cards/wallet/Overview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Coin from "@/components/ui/Coin";
import Tag from "@/components/ui/Tag";
import Currency from "@/components/ui/Currency";
import { Wallet } from "@/types/wallet";
import { useTranslation } from "next-i18next";

interface OverviewProps {
wallet: Wallet;
testId?: string
}

export default function Overview({ wallet, testId='overviewId' }: OverviewProps) {
const { t } = useTranslation();
return (
<div className="bg-secondary lg:w-60 md:w-60 sm:w-60 rounded-3.5xl" data-testid={testId}>
<div className="p-6">
<div className="border-b border-dotted border-gray-900">
<h1 className="text-2xl">{wallet.title}</h1>
<Tag value={wallet.token} />
<div className="text-right mb-4">
<Coin size="medium" token={wallet.token} />
</div>
</div>
<div className="flex">
<div className="w-1/2 pt-5 text-sm">
<h1>{t("profile.wallets.balance")}</h1>
</div>
<div className="w-1/2 pt-3.5 text-right text-2xl font-medium">
<h1>
<Currency value={wallet.balance} />
</h1>
</div>
</div>
</div>
</div>
);
}
23 changes: 23 additions & 0 deletions src/components/cards/wallet/WalletHint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Wallet } from "@/types/wallet";
import Currency from "@/components/ui/Currency";
import Hint from "@/components/ui/Hint";
import { useTranslation } from "next-i18next";

interface HintProps {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we use type for components props

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using interface in all components, I think we can stick to same approach

wallet: Wallet;
}
Comment on lines +6 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add prop types validation.

Consider adding prop types validation to ensure that the wallet prop conforms to the expected shape.

import PropTypes from 'prop-types';

interface HintProps {
  wallet: Wallet;
}

WalletHint.propTypes = {
  wallet: PropTypes.shape({
    payouts: PropTypes.arrayOf(
      PropTypes.shape({
        amount: PropTypes.number.isRequired,
        token: PropTypes.string.isRequired,
      })
    ).isRequired,
  }).isRequired,
};

export default function WalletHint({ wallet }: HintProps) {
const { t } = useTranslation();
return (
<>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need of fragements

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need them to return single JSX

{wallet.payouts.map((payout, i) => (
<Hint key={`wallet-payout-${i}`} className="mt-2">
<span className="font-medium">
<Currency value={payout.amount} token={payout.token} />
</span>{" "}
{t("profile.wallet.payout.text")}
</Hint>
))}
</>
);
}
Loading