Repository: joinmarket-webui/joinmarket-webui (js)

Issue:
Improve iconography for fidelity bonds
Currently we use a 🔒 for signalling both "frozen" UTXOs and UTXOs to be locked up in a fidelity bond. Opposite concepts, same icon. Should be changed. Probably best to pick another icon for frozen UTXOs.

Pull request:
fix: change icon for frozen UTXOs
Resolves #371.

Previously we used a 🔒 for signalling frozen UTXOs and UTXOs that will be locked up in a fidelity bond. Changed it so that we have:

- A snowflake ❄️ for frozen UTXOs (that will stay behind in the jar and thus be used for a fidelity bond).
- A lock 🔒 for UTXOs that will be locked up in a fidelity bond.

The snowflake might not be the ideal icon, I feel like an anchor ⚓ would be nice to say "this will stay and not move". But the snowflake was the best I could find on [bitcoinicons.com](https://bitcoinicons.com) and I didn't want to waste too much time looking around online.

Maybe we can update this in the future. A lock with a clock or an hourglass next to it would be a nice icon for fidelity bonds. Better than just a lock imo. Again, something for the future. cc @editwentyone what do you think?

## 📸 
 <table>
  <tr>
    <th>Frozen UTXO</th>
    <th>UTXOs for a fidelity bond</th>
  </tr>
  <tr>
    <td><img width="674" alt="Screenshot 2022-07-07 at 12 36 29" src="https://user-images.githubusercontent.com/10026790/177755824-be9d11da-adef-480b-a669-f460d87b1764.png"></td>
<td>
<img width="667" alt="Screenshot 2022-07-07 at 12 36 14" src="https://user-images.githubusercontent.com/10026790/177755811-0168b126-f668-4d96-bc9a-5d07df30fc6e.png"></td>
  </tr>
</table> 





Patch:
diff --git a/public/sprite.svg b/public/sprite.svg
index 6c7bc07..c5af708 100644
--- a/public/sprite.svg
+++ b/public/sprite.svg
@@ -288,4 +288,7 @@
         <path fill-rule="evenodd" d="M20.854 10.308c.6 2.411-.923 4.867-3.403 5.486-.31.077-.621.122-.928.137l-2.666 4.298a2 2 0 01-1.216.887l-1.236.308a1 1 0 01-1.212-.729l-.28-1.12a2 2 0 01.241-1.538l2.445-3.943a4.38 4.38 0 01-.728-1.547c-.6-2.411.922-4.867 3.403-5.486 2.48-.618 4.978.835 5.58 3.247zm-3.872 1.48c.552-.137.89-.683.756-1.219-.133-.536-.688-.859-1.24-.721-.55.137-.89.683-.756 1.219s.69.859 1.24.721z" clip-rule="evenodd"></path>
         <path fill-rule="evenodd" d="M11.251 12.187c2.48-.619 4.004-3.075 3.403-5.486-.601-2.412-3.1-3.865-5.58-3.247-2.48.618-4.004 3.075-3.402 5.486.143.575.394 1.096.728 1.547l-2.445 3.942a2 2 0 00-.241 1.538l.28 1.121a1 1 0 001.211.728l1.236-.308a2 2 0 001.216-.886l2.666-4.298c.307-.015.618-.06.928-.137zm.288-5.225c.133.536-.205 1.082-.756 1.219s-1.106-.186-1.24-.721c-.134-.536.205-1.082.756-1.22.551-.137 1.106.186 1.24.722z" clip-rule="evenodd"></path>
     </symbol >
+    <svg id="snowflake" viewBox="0 0 24 24" fill="currentColor">
+        <path fill-rule="evenodd" d="M12 3.25a.75.75 0 01.75.75v3.175l2.22-2.22a.75.75 0 111.06 1.06l-3.28 3.281v1.954h1.925l3.295-3.295a.75.75 0 111.06 1.06l-2.234 2.235H20a.75.75 0 010 1.5h-3.175l2.205 2.205a.75.75 0 11-1.06 1.06l-3.266-3.265H12.75v1.925l3.28 3.28a.75.75 0 11-1.06 1.06l-2.22-2.219V20a.75.75 0 01-1.5 0v-3.204l-2.22 2.22a.75.75 0 01-1.06-1.06l3.28-3.281V12.75H9.296L6.03 16.016a.75.75 0 01-1.06-1.06l2.205-2.206H4a.75.75 0 010-1.5h3.204L4.97 9.016a.75.75 0 011.06-1.06l3.295 3.294h1.925V9.296l-3.28-3.28a.75.75 0 111.06-1.06l2.22 2.219V4a.75.75 0 01.75-.75z" clip-rule="evenodd"></path>
+    </svg>
 </svg>
diff --git a/src/components/DisplayUTXOs.jsx b/src/components/DisplayUTXOs.jsx
index 3cd9e0d..c457c59 100644
--- a/src/components/DisplayUTXOs.jsx
+++ b/src/components/DisplayUTXOs.jsx
@@ -7,7 +7,7 @@ import Alert from './Alert'
 import { useSettings } from '../context/SettingsContext'
 import { useCurrentWallet } from '../context/WalletContext'
 import { useServiceInfo } from '../context/ServiceInfoContext'
-import { isLocked } from '../hooks/BalanceSummary'
+import * as fb from './fb/utils'
 import * as Api from '../libs/JmWalletApi'
 
 const Utxo = ({ utxo, ...props }) => {
@@ -22,7 +22,7 @@ const Utxo = ({ utxo, ...props }) => {
   const isOperationEnabled = useCallback(() => {
     const noServiceIsRunning = serviceInfo && !serviceInfo.makerRunning && !serviceInfo.coinjoinInProgress
 
-    const isUnfreezeEnabled = !isLocked(utxo)
+    const isUnfreezeEnabled = !fb.utxo.isLocked(utxo)
     const allowedToExecute = !utxo.frozen || isUnfreezeEnabled
 
     return noServiceIsRunning && allowedToExecute
diff --git a/src/components/fb/FidelityBondSteps.module.css b/src/components/fb/FidelityBondSteps.module.css
index 0062b9c..2e03521 100644
--- a/src/components/fb/FidelityBondSteps.module.css
+++ b/src/components/fb/FidelityBondSteps.module.css
@@ -126,7 +126,6 @@
 
 .utxoCard > .utxoLabel.utxoFrozen > svg {
   color: #2d9cdb;
-  margin-bottom: 0.1rem;
 }
 
 .utxoCard > .utxoLabel.utxoFidelityBond > svg {
diff --git a/src/components/fb/FidelityBondSteps.tsx b/src/components/fb/FidelityBondSteps.tsx
index 6e03f0f..c6b1e6d 100644
--- a/src/components/fb/FidelityBondSteps.tsx
+++ b/src/components/fb/FidelityBondSteps.tsx
@@ -108,6 +108,8 @@ const UtxoCard = ({
 }: UtxoCardProps) => {
   const settings = useSettings()
 
+  const utxoIsLocked = useMemo(() => fb.utxo.isLocked(utxo), [utxo])
+
   return (
     <div
       className={cx('utxoCard', { selected: isSelected, selectable: isSelectable })}
@@ -130,13 +132,13 @@ const UtxoCard = ({
           <rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" />
         </div>
       )}
-      {!isLoading && utxo.frozen && !utxo.locktime && (
+      {!isLoading && utxo.frozen && !utxoIsLocked && (
         <div className={cx('utxoLabel', 'utxoFrozen')}>
-          <Sprite symbol="lock" width="18" height="18" />
+          <Sprite symbol="snowflake" width="18" height="18" />
           <div>frozen</div>
         </div>
       )}
-      {!isLoading && utxo.locktime && (
+      {!isLoading && utxoIsLocked && (
         <div className={cx('utxoLabel', 'utxoFidelityBond')}>
           <Sprite symbol="lock" width="18" height="18" />
           <div>locked</div>
diff --git a/src/components/fb/utils.ts b/src/components/fb/utils.ts
index 8e7d7c5..0477be5 100644
--- a/src/components/fb/utils.ts
+++ b/src/components/fb/utils.ts
@@ -2,6 +2,7 @@ import { Lockdate } from '../../libs/JmWalletApi'
 import { Utxo } from '../../context/WalletContext'
 
 type Milliseconds = number
+type Seconds = number
 
 export type YearsRange = {
   min: number
@@ -91,5 +92,17 @@ export const utxo = (() => {
 
   const allAreFrozen = (utxos: Array<Utxo>) => utxos.every((utxo) => utxo.frozen)
 
-  return { isEqual, isInList, utxosToFreeze, allAreFrozen }
+  const isLocked = (utxo: Utxo, refTime: Milliseconds = Date.now()) => {
+    if (!utxo.locktime) return false
+
+    const pathAndLocktime = utxo.path.split(':')
+    if (pathAndLocktime.length !== 2) return false
+
+    const locktimeUnixTimestamp: Seconds = parseInt(pathAndLocktime[1], 10)
+    if (Number.isNaN(locktimeUnixTimestamp)) return false
+
+    return locktimeUnixTimestamp * 1_000 >= refTime
+  }
+
+  return { isEqual, isInList, utxosToFreeze, allAreFrozen, isLocked }
 })()
diff --git a/src/hooks/BalanceSummary.ts b/src/hooks/BalanceSummary.ts
index 29ff500..a797bc0 100644
--- a/src/hooks/BalanceSummary.ts
+++ b/src/hooks/BalanceSummary.ts
@@ -1,10 +1,10 @@
 import { useEffect, useMemo, useState } from 'react'
-import { WalletInfo, BalanceString, Utxos, Utxo } from '../context/WalletContext'
+import { WalletInfo, BalanceString, Utxos } from '../context/WalletContext'
 
+import * as fb from '../components/fb/utils'
 import { AmountSats } from '../libs/JmWalletApi'
 
 type Milliseconds = number
-type Seconds = number
 
 interface BalanceSummary {
   totalBalance: BalanceString
@@ -49,20 +49,8 @@ export type WalletBalanceSummary = BalanceSummarySupport & {
   accountBalances: AccountBalances
 }
 
-export const isLocked = (utxo: Utxo, refTime: Milliseconds = Date.now()) => {
-  if (!utxo.locktime) return false
-
-  const pathAndLocktime = utxo.path.split(':')
-  if (pathAndLocktime.length !== 2) return false
-
-  const locktimeUnixTimestamp: Seconds = parseInt(pathAndLocktime[1], 10)
-  if (Number.isNaN(locktimeUnixTimestamp)) return false
-
-  return locktimeUnixTimestamp * 1_000 >= refTime
-}
-
 const calculateFrozenOrLockedBalance = (utxos: Utxos, refTime: Milliseconds = Date.now()) => {
-  const frozenOrLockedUtxos = utxos.filter((utxo) => utxo.frozen || isLocked(utxo, refTime))
+  const frozenOrLockedUtxos = utxos.filter((utxo) => utxo.frozen || fb.utxo.isLocked(utxo, refTime))
   return frozenOrLockedUtxos.reduce((acc, utxo) => acc + utxo.value, 0)
 }
 
diff --git a/src/hooks/CoinjoinPrecondition.ts b/src/hooks/CoinjoinPrecondition.ts
index a931aeb..4c2d47d 100644
--- a/src/hooks/CoinjoinPrecondition.ts
+++ b/src/hooks/CoinjoinPrecondition.ts
@@ -1,5 +1,5 @@
 import { useMemo } from 'react'
-import { isLocked } from '../hooks/BalanceSummary'
+import * as fb from '../components/fb/utils'
 import { Utxos } from '../context/WalletContext'
 
 export const COINJOIN_PRECONDITIONS = {
@@ -18,7 +18,7 @@ export interface CoinjoinPreconditionSummary {
 
 export const useCoinjoinPreconditionSummary = (utxos: Utxos): CoinjoinPreconditionSummary => {
   const eligibleUtxos = useMemo(() => {
-    return utxos.filter((it) => !it.frozen).filter((it) => !isLocked(it))
+    return utxos.filter((it) => !it.frozen).filter((it) => !fb.utxo.isLocked(it))
   }, [utxos])
 
   return useMemo(() => {


Test patch:
diff --git a/src/components/fb/utils.test.ts b/src/components/fb/utils.test.ts
index 69fa812..6486776 100644
--- a/src/components/fb/utils.test.ts
+++ b/src/components/fb/utils.test.ts
@@ -165,5 +165,35 @@ describe('utils', () => {
       ]
       expect(fb.utxo.allAreFrozen(utxos)).toBe(false)
     })
+
+    describe('isLocked', () => {
+      const now = Date.UTC(2009, 0, 3)
+
+      it('should detect timelocked utxo as locked', () => {
+        const utxo = {
+          // timelocked, not yet expired
+          locktime: 'any',
+          path: `m/84'/1'/0'/0/1:${now / 1_000 + 1}`,
+        } as Utxo
+        expect(fb.utxo.isLocked(utxo, now)).toBe(true)
+      })
+
+      it('should detect expired timelocked utxo as unlocked', () => {
+        const utxo = {
+          // timelocked, but expired
+          locktime: 'any',
+          path: `m/84'/1'/0'/0/1:${now / 1_000 - 1}`,
+        } as Utxo
+        expect(fb.utxo.isLocked(utxo, now)).toBe(false)
+      })
+
+      it('should detect non-timelocked utxo as unlocked', () => {
+        const utxo = {
+          // not timelocked
+          path: `m/84'/1'/0'/0/1`,
+        } as Utxo
+        expect(fb.utxo.isLocked(utxo, now)).toBe(false)
+      })
+    })
   })
 })
diff --git a/src/hooks/BalanceSummary.test.tsx b/src/hooks/BalanceSummary.test.tsx
index 83391de..0b556e1 100644
--- a/src/hooks/BalanceSummary.test.tsx
+++ b/src/hooks/BalanceSummary.test.tsx
@@ -2,39 +2,11 @@ import React from 'react'
 import { render } from '../testUtils'
 import { act } from 'react-dom/test-utils'
 
-import { useBalanceSummary, WalletBalanceSummary, isLocked } from './BalanceSummary'
+import { useBalanceSummary, WalletBalanceSummary } from './BalanceSummary'
 import { WalletInfo, Utxo } from '../context/WalletContext'
 
 const now = Date.UTC(2009, 0, 3)
 
-describe('isLocked', () => {
-  it('should detect timelocked utxo as locked', () => {
-    const utxo = {
-      // timelocked, not yet expired
-      locktime: 'any',
-      path: `m/84'/1'/0'/0/1:${now / 1_000 + 1}`,
-    } as Utxo
-    expect(isLocked(utxo, now)).toBe(true)
-  })
-
-  it('should detect expired timelocked utxo as unlocked', () => {
-    const utxo = {
-      // timelocked, but expired
-      locktime: 'any',
-      path: `m/84'/1'/0'/0/1:${now / 1_000 - 1}`,
-    } as Utxo
-    expect(isLocked(utxo, now)).toBe(false)
-  })
-
-  it('should detect non-timelocked utxo as unlocked', () => {
-    const utxo = {
-      // not timelocked
-      path: `m/84'/1'/0'/0/1`,
-    } as Utxo
-    expect(isLocked(utxo, now)).toBe(false)
-  })
-})
-
 describe('BalanceSummary', () => {
   function setup(walletInfo: WalletInfo | null, refTime: number) {
     const returnVal: { data: WalletBalanceSummary | null | undefined } = { data: undefined }