Skip to content

Wallet Detail - Action Summary & Action Logs for stewards #221 - #297

Merged
L03TJ3 merged 25 commits into
GoodDollar:masterfrom
EmekaManuel:td-221-implement-action-summary
Jul 28, 2025
Merged

Wallet Detail - Action Summary & Action Logs for stewards #221#297
L03TJ3 merged 25 commits into
GoodDollar:masterfrom
EmekaManuel:td-221-implement-action-summary

Conversation

@EmekaManuel

@EmekaManuel EmekaManuel commented Jul 14, 2025

Copy link
Copy Markdown
Contributor

@sirpy @L03TJ3
#221

Description

This PR implements the Wallet Detail - Action Summary & Action Logs feature for stewards (#221). . The changes add comprehensive activity tracking and display functionality for steward wallets.

Screenshot 2025-07-14 at 20 32 21 Screenshot 2025-07-14 at 20 32 43 Screenshot 2025-07-14 at 20 32 32 Screenshot 2025-07-14 at 20 32 54

Key Features Added:

  • Enhanced ActivityLog Component: Completely redesigned UI with expandable/collapsible functionality showing NFT details, payment transactions, and IPFS verification links
  • Activity Log Page: New dedicated page (/profile/{address}/activity) displaying user profiles with detailed action logs organized by collective
  • Steward Collective Card Enhancement: Added clickable action counts that navigate to the activity log page
  • Activity Data Hooks: New custom hooks for fetching and organizing activity data from subgraph
  • External Link Integration: Direct links to Celoscan for transaction details and IPFS gateways for verification documents

Technical Implementation:

  • New useActivityLogData and useActivityLogByCollective hooks for data management
  • Enhanced subgraph integration with useSubgraphStewardWithActivityData
  • Responsive design with proper styling and user interactions
  • External URL handling with fallback support for IPFS gateways

How Has This Been Tested?

The changes have been tested through:

  • UI/UX Testing: Verified expandable activity logs with proper toggle functionality
  • Navigation Testing: Confirmed clickable action counts properly route to activity pages
  • External Link Testing: Validated Celoscan and IPFS gateway links open correctly
  • Data Integration Testing: Ensured activity data displays accurately from subgraph sources
  • Responsive Design Testing: Confirmed proper layout across different screen sizes
  • Edge Case Testing: Verified behavior with missing or incomplete data

Files Changed:

  • ActivityLog.tsx - Complete redesign with expandable functionality
  • ActivityLogPage.tsx - New comprehensive activity display page
  • StewardCollectiveCard.tsx - Added clickable action navigation
  • WalletCards.tsx & WalletProfile.tsx - Props threading for steward address
  • useActivityLogData.ts - New data management hooks

Dependencies:

  • Uses existing navigation and styling systems
  • Integrates with current collective and steward data models

Description by Korbit AI

What change is being made?

Implement Activity Summary and Action Logs for Steward profiles in the wallet application.

Why are these changes being made?

These changes are introduced to provide stewards with a detailed view of their activities within their wallet profiles, enabling them to track and manage their environmental actions and transactions effectively. This is achieved through an enriched UI that includes the ability to expand and view detailed logs, as well as handling dynamic links to external resources such as blockchain explorers and IPFS. The solution leverages new hooks and utilities to seamlessly fetch and format data, while addressing potential UI and data transformation gaps.

Is this description stale? Ask me to generate a new description by commenting /korbit-generate-pr-description

@korbit-ai

korbit-ai Bot commented Jul 14, 2025

Copy link
Copy Markdown

Korbit doesn't automatically review large (3000+ lines changed) pull requests such as this one. If you want me to review anyway, use /korbit-review.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey @EmekaManuel - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `packages/app/src/components/ActivityLog.tsx:67` </location>
<code_context>
+
+  const truncatedName = name.length > 15 ? `${name.substring(0, 5)}...${name.substring(name.length - 5)}` : name;
+
+  const handleIpfsPress = async () => {
+    if (ipfsHash) {
+      const cleanHash = ipfsHash.replace(/^(ipfs:\/\/|https?:\/\/[^/]+\/ipfs\/)/, '');
+      const gateways = [
+        `https://ipfs.io/ipfs/${cleanHash}`,
+        `https://gateway.pinata.cloud/ipfs/${cleanHash}`,
+        `https://cloudflare-ipfs.com/ipfs/${cleanHash}`,
+      ];
+
+      try {
+        await openExternalLink(gateways[0]);
+      } catch (error) {
+        try {
+          await openExternalLink(gateways[1]);
+        } catch (secondError) {
+          console.warn('IPFS gateways failed');
+        }
+      }
</code_context>

<issue_to_address>
IPFS gateway fallback logic may not work as intended due to openExternalLink always resolving.

Since openExternalLink only verifies if a URL can be opened, not if the resource exists, the fallback gateways may never be used even if the first is down. Consider checking resource availability with a fetch or HEAD request before attempting to open the link, or document that this is a best-effort fallback.
</issue_to_address>

### Comment 2
<location> `packages/app/src/components/ActivityLog.tsx:88` </location>
<code_context>
+    }
+  };
+
+  const hasPaymentData = Boolean(transactionHash || hash || owner);
+
   return (
</code_context>

<issue_to_address>
hasPaymentData logic may enable Payment Transaction button even if only owner is present.

Currently, the button is enabled when only owner is present, which may not correspond to a transaction. Should the button be disabled unless a transaction hash exists?
</issue_to_address>

### Comment 3
<location> `packages/app/src/pages/ActivityLogPage.tsx:28` </location>
<code_context>
+
+  const { data: ensName } = useEnsName({ address, chainId: 1 });
+  const fullName = useFetchFullName(address);
+  const [firstName, lastName] = fullName?.trim().split(' ') ?? [undefined, undefined];
+  const userIdentifier = firstName ? `${firstName} ${lastName}` : ensName ?? address ?? '0x';
+  const isWhitelisted = useIsStewardVerified(address as `0x${string}`);
</code_context>

<issue_to_address>
Splitting fullName by space may not handle multi-part names correctly.

This approach may fail for names with more or fewer than two parts. Consider splitting on the first space or handling cases with missing or extra name parts.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
  const fullName = useFetchFullName(address);
  const [firstName, lastName] = fullName?.trim().split(' ') ?? [undefined, undefined];
  const userIdentifier = firstName ? `${firstName} ${lastName}` : ensName ?? address ?? '0x';
=======
  const fullName = useFetchFullName(address);
  let firstName: string | undefined, lastName: string | undefined;
  if (fullName && fullName.trim().length > 0) {
    const trimmed = fullName.trim();
    const firstSpaceIdx = trimmed.indexOf(' ');
    if (firstSpaceIdx === -1) {
      firstName = trimmed;
      lastName = undefined;
    } else {
      firstName = trimmed.slice(0, firstSpaceIdx);
      lastName = trimmed.slice(firstSpaceIdx + 1);
    }
  } else {
    firstName = undefined;
    lastName = undefined;
  }
  const userIdentifier = firstName ? (lastName ? `${firstName} ${lastName}` : firstName) : ensName ?? address ?? '0x';
>>>>>>> REPLACE

</suggested_fix>

### Comment 4
<location> `packages/app/src/pages/ActivityLogPage.tsx:85` </location>
<code_context>
+      return selectedCollectiveActivities;
+    }
+
+    if (steward?.nfts && steward.nfts.length > 0) {
+      return steward.nfts
+        .filter((nft) => !selectedCollective || nft.collective === selectedCollective)
+        .map((nft, _index) => ({
+          id: nft.id,
+          name: nft.id,
+          creationDate: new Date().toLocaleDateString(),
+          nftId: nft.id,
+          hash: nft.hash,
+          owner: nft.owner,
+          collective: nft.collective,
+          ipfsHash: nft.hash,
+        }));
+    }
+
</code_context>

<issue_to_address>
Fallback to steward.nfts may result in duplicate or inconsistent activity data.

Please clarify which data source should take precedence and ensure the fallback logic does not cause data duplication or inconsistencies.
</issue_to_address>

### Comment 5
<location> `packages/app/src/hooks/useActivityLogData.ts:86` </location>
<code_context>
+  });
+}
+
+function formatNftId(nftId: string, collectiveId: string): string {
+  const prefix = collectiveId.substring(0, 3).toUpperCase();
+  const suffix = nftId.substring(nftId.length - 4);
+  const year = new Date().getFullYear();
+  return `#${prefix}-${year}-${suffix}`;
+}
+
</code_context>

<issue_to_address>
formatNftId uses current year, which may not match NFT creation year.

Consider passing the NFT's creation year to formatNftId to avoid displaying incorrect years for older NFTs.
</issue_to_address>

### Comment 6
<location> `packages/app/src/hooks/useActivityLogData.ts:109` </location>
<code_context>
+  return fallbackNames[pooltype] || 'Environmental Collective';
+}
+
+function formatAmount(amount: string): string {
+  try {
+    const amountBigInt = BigInt(amount);
+    const decimals = 18;
+    const divisor = BigInt(10 ** decimals);
+    const wholePart = amountBigInt / divisor;
+    const fractionalPart = amountBigInt % divisor;
+
+    if (fractionalPart === 0n) {
+      return wholePart.toString();
+    }
+
+    const fractionalStr = fractionalPart.toString().padStart(decimals, '0');
+    const trimmedFractional = fractionalStr.substring(0, 3).replace(/0+$/, '');
+
+    if (trimmedFractional === '') {
+      return wholePart.toString();
+    }
+
+    return `${wholePart}.${trimmedFractional}`;
+  } catch (error) {
+    return '0.758';
+  }
+}
</code_context>

<issue_to_address>
Hardcoded fallback value in formatAmount may be misleading.

Returning '0.758' on parse failure may mislead users. Use '0' or an explicit error indicator to avoid confusion.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    return '0.758';
=======
    return '0';
>>>>>>> REPLACE

</suggested_fix>

### Comment 7
<location> `packages/app/src/hooks/useActivityLogData.ts:20` </location>
<code_context>
+  timestamp: number;
+}
+
+export function useActivityLogData(stewardId: string): ActivityLogItem[] {
+  const activityData = useSubgraphStewardWithActivityData(stewardId);
+
</code_context>

<issue_to_address>
Consider extracting formatting and lookup helpers into separate utility modules and flattening nested loops with array methods to keep the hook focused on data mapping.

Here are two quick wins to collapse that hook down to just “data‐stuff” and move all the formatting/lookups out:

1) Extract your helpers into small utils modules  
2) Flatten your nested loops with `flatMap` + `filter` + `map`  

---  
### 1) utils/formatDate.ts  
```ts
// src/utils/formatDate.ts
export function formatDate(ts: number): string {
  return new Date(ts * 1000).toLocaleDateString('en-US', {
    year: 'numeric', month: 'long', day: 'numeric',
  });
}
```  
### 2) utils/formatNftId.ts  
```ts
// src/utils/formatNftId.ts
export function formatNftId(nftId: string, collectiveId: string): string {
  const prefix = collectiveId.slice(0,3).toUpperCase();
  const suffix = nftId.slice(-4);
  return `#${prefix}-${new Date().getFullYear()}-${suffix}`;
}
```  
### 3) utils/formatAmount.ts  
```ts
// src/utils/formatAmount.ts
export function formatAmount(amount: string): string {
  try {
    const value = BigInt(amount), dec = BigInt(10 ** 18);
    const whole = value / dec;
    const frac  = (value % dec).toString().padStart(18,'0').slice(0,3).replace(/0+$/,'');
    return frac ? `${whole}.${frac}` : whole.toString();
  } catch {
    return '0.758';
  }
}
```  
### 4) utils/names.ts  
```ts
// src/utils/names.ts
const ACTIVITY: Record<string,string> = {
  Climate: 'Silvi Proof of Planting',
  DirectPayments: 'Environmental Action Proof',
  UBI: 'UBI Claim',
};
const COLLECTIVE: Record<string,string> = {
  Climate: 'Restoring the Kakamega Forest',
  DirectPayments: 'Environmental Collective',
  UBI: 'UBI Collective',
};
export function getActivityName(type: string) {
  return ACTIVITY[type] || 'Environmental Action';
}
export function getCollectiveName(id: string, type: string) {
  // if you eventually have a map of custom names, merge here:
  return COLLECTIVE[type] || 'Environmental Collective';
}
```  

---  
### Updated hook (only data‐mapping)  
```ts
import { useMemo } from 'react';
import { formatDate } from '../utils/formatDate';
import { formatNftId } from '../utils/formatNftId';
import { formatAmount } from '../utils/formatAmount';
import { getActivityName, getCollectiveName } from '../utils/names';

// ...

export function useActivityLogData(stewardId: string): ActivityLogItem[] {
  const data = useSubgraphStewardWithActivityData(stewardId);

  return useMemo(() => {
    if (!data?.claims) return [];

    return data.claims
      .flatMap(claim =>
        claim.events
          .filter(evt =>
            evt.nft &&
            evt.contributors.some(c => c.id.toLowerCase() === stewardId.toLowerCase())
          )
          .map(evt => ({
            id: evt.id,
            name: getActivityName(claim.collective.pooltype),
            creationDate: formatDate(evt.timestamp),
            nftId: formatNftId(evt.nft.id, claim.collective.id),
            nftHash: evt.nft.id,
            ipfsHash: evt.nft.hash,
            paymentAmount: `${formatAmount(evt.rewardPerContributor)} ${claim.settings.rewardToken}`,
            transactionHash: claim.txHash,
            collective: {
              id: claim.collective.id,
              name: getCollectiveName(claim.collective.id, claim.collective.pooltype),
            },
            timestamp: evt.timestamp,
          }))
      )
      .sort((a, b) => b.timestamp - a.timestamp);
  }, [data, stewardId]);
}
```

This way the hook stays focused on data‐flow, and all formatting/lookups live in tiny, reusable modules.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +67 to +76
const handleIpfsPress = async () => {
if (ipfsHash) {
const cleanHash = ipfsHash.replace(/^(ipfs:\/\/|https?:\/\/[^/]+\/ipfs\/)/, '');
const gateways = [
`https://ipfs.io/ipfs/${cleanHash}`,
`https://gateway.pinata.cloud/ipfs/${cleanHash}`,
`https://cloudflare-ipfs.com/ipfs/${cleanHash}`,
];

try {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): IPFS gateway fallback logic may not work as intended due to openExternalLink always resolving.

Since openExternalLink only verifies if a URL can be opened, not if the resource exists, the fallback gateways may never be used even if the first is down. Consider checking resource availability with a fetch or HEAD request before attempting to open the link, or document that this is a best-effort fallback.

}
};

const hasPaymentData = Boolean(transactionHash || hash || owner);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question (bug_risk): hasPaymentData logic may enable Payment Transaction button even if only owner is present.

Currently, the button is enabled when only owner is present, which may not correspond to a transaction. Should the button be disabled unless a transaction hash exists?

Comment on lines +27 to +29
const fullName = useFetchFullName(address);
const [firstName, lastName] = fullName?.trim().split(' ') ?? [undefined, undefined];
const userIdentifier = firstName ? `${firstName} ${lastName}` : ensName ?? address ?? '0x';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Splitting fullName by space may not handle multi-part names correctly.

This approach may fail for names with more or fewer than two parts. Consider splitting on the first space or handling cases with missing or extra name parts.

Suggested change
const fullName = useFetchFullName(address);
const [firstName, lastName] = fullName?.trim().split(' ') ?? [undefined, undefined];
const userIdentifier = firstName ? `${firstName} ${lastName}` : ensName ?? address ?? '0x';
const fullName = useFetchFullName(address);
let firstName: string | undefined, lastName: string | undefined;
if (fullName && fullName.trim().length > 0) {
const trimmed = fullName.trim();
const firstSpaceIdx = trimmed.indexOf(' ');
if (firstSpaceIdx === -1) {
firstName = trimmed;
lastName = undefined;
} else {
firstName = trimmed.slice(0, firstSpaceIdx);
lastName = trimmed.slice(firstSpaceIdx + 1);
}
} else {
firstName = undefined;
lastName = undefined;
}
const userIdentifier = firstName ? (lastName ? `${firstName} ${lastName}` : firstName) : ensName ?? address ?? '0x';

id: collective.collective,
name: stewardIpfsCollectives[index]?.name || `Collective ${index + 1}`,
count: collective.actions || steward.nfts?.length || 0,
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question (bug_risk): Fallback to steward.nfts may result in duplicate or inconsistent activity data.

Please clarify which data source should take precedence and ensure the fallback logic does not cause data duplication or inconsistencies.

Comment on lines +86 to +90
function formatNftId(nftId: string, collectiveId: string): string {
const prefix = collectiveId.substring(0, 3).toUpperCase();
const suffix = nftId.substring(nftId.length - 4);
const year = new Date().getFullYear();
return `#${prefix}-${year}-${suffix}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): formatNftId uses current year, which may not match NFT creation year.

Consider passing the NFT's creation year to formatNftId to avoid displaying incorrect years for older NFTs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

True, nft's could have been created last year. please fix

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You're absolutely right—using the current year could misrepresent the actual creation year of the NFT. To fix this, you should update the formatNftId function to accept the NFT's creation timestamp (or year) as an argument, and use that value instead of new Date().getFullYear().

Here's how you could adjust the function:

function formatNftId(nftId: string, collectiveId: string, timestamp: number): string {
  const prefix = collectiveId.substring(0, 3).toUpperCase();
  const suffix = nftId.substring(nftId.length - 4);
  const year = new Date(timestamp * 1000).getFullYear();
  return `#${prefix}-${year}-${suffix}`;
}

And when calling formatNftId, pass the NFT's creation timestamp (likely event.timestamp or a similar field) as the third argument.

Let me know if you need help updating the call sites or have any other questions!


return `${wholePart}.${trimmedFractional}`;
} catch (error) {
return '0.758';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Hardcoded fallback value in formatAmount may be misleading.

Returning '0.758' on parse failure may mislead users. Use '0' or an explicit error indicator to avoid confusion.

Suggested change
return '0.758';
return '0';

Comment thread packages/app/src/hooks/useActivityLogData.ts

@L03TJ3 L03TJ3 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What are the changes done to deployment.json?

@@ -0,0 +1,105 @@
import { gql } from '@apollo/client';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We have useSubgraphSteward already which contains most of this.

Just extend with any missing fields.
For example the claim-events as shared on TG:

    collectives {
     pooltype
      collective {
        claims {
          events {
            id
          }
        }
      }
    }

The ID of the event when poolType is: DirectPayments
holds the ipfs URI

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

when i try extending the missing fields, the application breaks

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@EmekaManuel

Copy link
Copy Markdown
Contributor Author

What are the changes done to deployment.json?

i made no change to deployment.json. It is most likely prettier formatting

@L03TJ3

L03TJ3 commented Jul 21, 2025

Copy link
Copy Markdown
Collaborator

If no changes, please revert to previous version. it should only have changes upon deploying a new contract @EmekaManuel

@EmekaManuel

Copy link
Copy Markdown
Contributor Author

If no changes, please revert to previous version. it should only have changes upon deploying a new contract @EmekaManuel

i have reverted the changes @L03TJ3

Comment thread packages/app/src/components/ActivityLog.tsx

@L03TJ3 L03TJ3 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  1. Follow up on the comments,
  2. replace the new subgraph hook by extending the existing one.

Comment thread packages/app/src/pages/ActivityLogPage.tsx Outdated
paymentAmount,
timestamp,
}: ActivityLogProps) {
const NFT_CA = '0x251EEBd7d9469bbcc02Ef23c95D902Cbb7fD73B3';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This would not work hardcoded on all environments.
please read the approriate contract-address from the deployment.json.

see constants.ts
how to read from the deployment.json, and use the configured network (SupportedNetworkNames) to get the right contract based on network/env

return `${wholePart}.${trimmedFractional}`;
} catch (error) {
return '0.758';
console.warn('Error formatting amount:', error);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

formatAmount is not needed.
You can just do Number(amount / 1e18).toFixed(3)

}
};

const formatDate = (dateString: string, timestampValue: number) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  1. methods like this should be helper methods outside of a hook (very likely to be re-used). now you have the same method in two places (Not following DRY principle)
  2. we put helper methods usually in /lib or /utils.
  3. in /lib, you will see there is already a formatTime helper. use that one

Comment thread packages/app/src/hooks/useActivityLogData.ts
Comment on lines +86 to +90
function formatNftId(nftId: string, collectiveId: string): string {
const prefix = collectiveId.substring(0, 3).toUpperCase();
const suffix = nftId.substring(nftId.length - 4);
const year = new Date().getFullYear();
return `#${prefix}-${year}-${suffix}`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

True, nft's could have been created last year. please fix

: undefined;

const { data: ensName } = useEnsName({ address, chainId: 1 });
const fullName = useFetchFullName(address);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No need for fullname handling

}, {} as Record<string, { name: string; count: number }>)
: {};

const displayCollectives = (() => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

if this needs to be run on mount (seeing it being a IIFE).

use useEffect

Comment thread packages/app/src/utils/formatAmount.ts Outdated
@@ -0,0 +1,11 @@
export function formatAmount(amount: string): string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There is no need for this complication.

Wherever you need to do this calculation just do Number(amount * 1e18).toFixed(3).

> amount = 500334455600000000000
500334455600000000000
> Number(amount / 1e18).toFixed(3)
'500.334'

Comment thread packages/app/src/utils/formatDate.ts Outdated
@@ -0,0 +1,7 @@
export function formatDate(ts: number): string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

there is formatTime utility, no need for a new one

… display logic using useMemo and useEffect
@L03TJ3
L03TJ3 merged commit 2e2f8ad into GoodDollar:master Jul 28, 2025
3 checks passed
@L03TJ3 L03TJ3 linked an issue Nov 21, 2025 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wallet Detail - Action Summary & Action Logs for stewards

2 participants