diff --git a/public/sprite.svg b/public/sprite.svg index 11e74fc6a..6d06bafe6 100644 --- a/public/sprite.svg +++ b/public/sprite.svg @@ -23,6 +23,7 @@ + diff --git a/src/components/App.jsx b/src/components/App.jsx index cae8b3464..a71d3f849 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -12,27 +12,31 @@ import CurrentWalletAdvanced from './CurrentWalletAdvanced' import Settings from './Settings' import Navbar from './Navbar' import Layout from './Layout' -import { useSettings } from '../context/SettingsContext' +import Sprite from './Sprite' +import { useSettings, useSettingsDispatch } from '../context/SettingsContext' import { useWebsocketState } from '../context/WebsocketContext' import { useCurrentWallet, useSetCurrentWallet } from '../context/WalletContext' import { useSessionConnectionError } from '../context/ServiceInfoContext' import { setSession, clearSession } from '../session' import Onboarding from './Onboarding' -import Sprite from './Sprite' +import Cheatsheet from './Cheatsheet' import { routes } from '../constants/routes' +import { isFeatureEnabled } from '../constants/featureFlags' export default function App() { const { t } = useTranslation() + const settings = useSettings() + const settingsDispatch = useSettingsDispatch() + const websocketState = useWebsocketState() const currentWallet = useCurrentWallet() const setCurrentWallet = useSetCurrentWallet() const sessionConnectionError = useSessionConnectionError() const [websocketConnected, setWebsocketConnected] = useState() const [showAlphaWarning, setShowAlphaWarning] = useState(false) - const settings = useSettings() - const websocketState = useWebsocketState() + const [showCheatsheet, setShowCheatsheet] = useState(false) - const devMode = process.env.NODE_ENV === 'development' + const cheatsheetEnabled = currentWallet && isFeatureEnabled('cheatsheet') const startWallet = useCallback( (name, token) => { @@ -52,6 +56,19 @@ export default function App() { setWebsocketConnected(websocketState === WebSocket.OPEN) }, [websocketState]) + useEffect(() => { + let timer + // show the cheatsheet once after the first wallet has been created + if (cheatsheetEnabled && settings.showCheatsheet) { + timer = setTimeout(() => { + setShowCheatsheet(true) + settingsDispatch({ showCheatsheet: false }) + }, 1_000) + } + + return () => clearTimeout(timer) + }, [cheatsheetEnabled, settings, settingsDispatch]) + if (settings.showOnboarding === true) { return ( @@ -95,7 +112,7 @@ export default function App() { * that it stays visible in case the backend becomes unavailable. */} }> - } /> + } /> {/** * This section defines all routes that are displayed only if the backend is reachable. @@ -128,6 +145,7 @@ export default function App() { )} +
@@ -144,47 +162,66 @@ export default function App() {
-
- - - {t('footer.docs')} - - - - - {t('footer.features')} - - - - - {t('footer.github')} - - - - - {t('footer.twitter')} - - +
+ {cheatsheetEnabled && ( +
+ setShowCheatsheet(false)} /> + + setShowCheatsheet(true)} + > +
+ +
{t('footer.cheatsheet')}
+
+
+
+
+ )} +
+ + + {t('footer.docs')} + + + + + {t('footer.features')} + + + + + {t('footer.github')} + + + + + {t('footer.twitter')} + + +
void +} + +type NumberedProps = { + number: number + className?: string +} + +function Numbered({ number }: { number: number }) { + return
{number}
+} + +function ListItem({ number, children, ...props }: PropsWithChildren) { + return ( + + + {children} + + ) +} + +export default function Cheatsheet({ show = false, onHide }: CheatsheetProps) { + const { t } = useTranslation() + + return ( + + + + {t('cheatsheet.title')} +
+ + Follow the steps below to increase your financial privacy. It is advisable to switch from{' '} + + earning as a maker + {' '} + to{' '} + + sending as a taker + {' '} + back and forth.{' '} + + Learn more. + + +
+
+
+ + + +
+ + Fund your wallet. + +
+
{t('cheatsheet.item_1.description')}
+
+ +
+ + Send a collaborative transaction to yourself. + +
+
{t('cheatsheet.item_2.description')}
+
+ +
+ + Optional: Lock funds in a fidelity bond. + +
+
+ {t('cheatsheet.item_3.description')} +
+ {/* the following phrase is intentionally not translated because it will be removed soon */} + Feature not implemented yet. Coming soon! +
+
+ +
+ + Earn yield by providing liquidity. + +
+
{t('cheatsheet.item_4.description')}
+
+ +
+ + Schedule transactions. + +
+
{t('cheatsheet.item_5.description')}
+
+ +
{t('cheatsheet.item_6.title')}
+
+ + Still confused?{' '} + + Dig into the documentation + + . + +
+
+
+
+
+ ) +} diff --git a/src/components/CreateWallet.jsx b/src/components/CreateWallet.jsx index d10219c40..1df612c8d 100644 --- a/src/components/CreateWallet.jsx +++ b/src/components/CreateWallet.jsx @@ -11,6 +11,7 @@ import { useServiceInfo } from '../context/ServiceInfoContext' import * as Api from '../libs/JmWalletApi' import './CreateWallet.css' import { routes } from '../constants/routes' +import { isFeatureEnabled } from '../constants/featureFlags' const PreventLeavingPageByMistake = () => { // prompt users before refreshing or closing the page when this component is present. @@ -173,13 +174,13 @@ const SeedWordInput = ({ number, targetWord, isValid, setIsValid }) => { ) } -const BackupConfirmation = ({ createdWallet, walletConfirmed, parentStepSetter, devMode }) => { +const BackupConfirmation = ({ createdWallet, walletConfirmed, parentStepSetter }) => { const seedphrase = createdWallet.seedphrase.split(' ') const { t } = useTranslation() const [seedBackup, setSeedBackup] = useState(false) const [seedWordConfirmations, setSeedWordConfirmations] = useState(new Array(seedphrase.length).fill(false)) - const [showSkipButton] = useState(devMode) + const [showSkipButton] = useState(isFeatureEnabled('skipWalletBackupConfirmation')) useEffect(() => { setSeedBackup(seedWordConfirmations.every((wordConfirmed) => wordConfirmed)) @@ -250,7 +251,7 @@ const BackupConfirmation = ({ createdWallet, walletConfirmed, parentStepSetter, ) } -const WalletCreationConfirmation = ({ createdWallet, walletConfirmed, devMode }) => { +const WalletCreationConfirmation = ({ createdWallet, walletConfirmed }) => { const { t } = useTranslation() const [userConfirmed, setUserConfirmed] = useState(false) const [revealSensitiveInfo, setRevealSensitiveInfo] = useState(false) @@ -306,14 +307,13 @@ const WalletCreationConfirmation = ({ createdWallet, walletConfirmed, devMode }) parentStepSetter={childStepSetter} createdWallet={createdWallet} walletConfirmed={walletConfirmed} - devMode={devMode} /> )} ) } -export default function CreateWallet({ startWallet, devMode = false }) { +export default function CreateWallet({ startWallet }) { const { t } = useTranslation() const serviceInfo = useServiceInfo() const navigate = useNavigate() @@ -362,9 +362,7 @@ export default function CreateWallet({ startWallet, devMode = false }) { )} {alert && {alert.message}} {canCreate && } - {isCreated && ( - - )} + {isCreated && } {!canCreate && !isCreated && ( diff --git a/src/components/CreateWallet.test.jsx b/src/components/CreateWallet.test.jsx index fdc53581f..38842b30a 100644 --- a/src/components/CreateWallet.test.jsx +++ b/src/components/CreateWallet.test.jsx @@ -2,6 +2,7 @@ import React from 'react' import user from '@testing-library/user-event' import { render, screen, waitFor, waitForElementToBeRemoved } from '../testUtils' import { act } from 'react-dom/test-utils' +import { __testSetFeatureEnabled } from '../constants/featureFlags' import * as apiMock from '../libs/JmWalletApi' @@ -21,8 +22,7 @@ describe('', () => { const setup = (props) => { const startWallet = props?.startWallet || NOOP - const devMode = props?.devMode || false - render() + render() } beforeEach(() => { @@ -114,7 +114,7 @@ describe('', () => { expect(screen.queryByText('create_wallet.button_create')).not.toBeInTheDocument() }) - it('should verify that "skip" button is NOT visible when not in development mode', async () => { + it('should verify that "skip" button is NOT visible by default (feature is disabled)', async () => { apiMock.postWalletCreate.mockResolvedValueOnce({ ok: true, json: () => @@ -125,7 +125,7 @@ describe('', () => { }), }) - act(() => setup({ devMode: false })) + act(setup) act(() => { user.type(screen.getByPlaceholderText('create_wallet.placeholder_wallet_name'), testWalletName) @@ -154,7 +154,9 @@ describe('', () => { expect(screen.getByText('create_wallet.confirmation_button_fund_wallet')).toBeDisabled() }) - it('should verify that "skip" button IS visible in development mode', async () => { + it('should verify that "skip" button IS visible when feature is enabled', async () => { + __testSetFeatureEnabled('skipWalletBackupConfirmation', true) + apiMock.postWalletCreate.mockResolvedValueOnce({ ok: true, json: () => @@ -165,7 +167,7 @@ describe('', () => { }), }) - act(() => setup({ devMode: true })) + act(setup) act(() => { user.type(screen.getByPlaceholderText('create_wallet.placeholder_wallet_name'), testWalletName) diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 571cf4bff..22927cba7 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -166,7 +166,7 @@ export default function Navbar() { {t('navbar.menu')}
- + {t('navbar.title')} @@ -212,7 +212,7 @@ export default function Navbar() { {t('navbar.menu_mobile')}
- + {t('navbar.title')} diff --git a/src/constants/featureFlags.ts b/src/constants/featureFlags.ts new file mode 100644 index 000000000..ab33e3276 --- /dev/null +++ b/src/constants/featureFlags.ts @@ -0,0 +1,22 @@ +interface FeatureFlags { + skipWalletBackupConfirmation: boolean + cheatsheet: boolean +} + +const devMode = process.env.NODE_ENV === 'development' + +const featureFlags: FeatureFlags = { + skipWalletBackupConfirmation: devMode, + cheatsheet: devMode, +} + +type FeatureFlag = keyof FeatureFlags + +export const isFeatureEnabled = (name: FeatureFlag): boolean => { + return featureFlags[name] || false +} + +// only to be used in tests +export const __testSetFeatureEnabled = (name: FeatureFlag, enabled: boolean): boolean => { + return (featureFlags[name] = enabled) +} diff --git a/src/context/SettingsContext.jsx b/src/context/SettingsContext.jsx index 57dcbcb6c..63f5150fa 100644 --- a/src/context/SettingsContext.jsx +++ b/src/context/SettingsContext.jsx @@ -7,6 +7,7 @@ const initialSettings = { showBalance: false, unit: BTC, showOnboarding: true, + showCheatsheet: true, useAdvancedWalletMode: false, } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 29db35102..1a72534b5 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -18,6 +18,7 @@ "warning_alert_title": "Warning", "warning_alert_text": "While JoinMarket is tried and tested, Jam is not. It is in an alpha stage, so use with caution.", "warning_alert_button_ok": "Fine with me.", + "cheatsheet": "Cheatsheet", "docs": "Docs", "features": "Features", "github": "GitHub", @@ -226,5 +227,33 @@ "text_stopping": "Stopping", "button_show_report": "Show report", "button_hide_report": "Hide report" + }, + "cheatsheet": { + "title": "The Cheatsheet", + "description": "Follow the steps below to increase your financial privacy. It is advisable to switch from <2>earning as a maker to <6>sending as a taker back and forth. <10>Learn more.", + "item_1": { + "title": "<0>Fund your wallet.", + "description": "Deposit some funds yourself or receive it from others." + }, + "item_2": { + "title": "<0>Send a collaborative transaction to yourself.", + "description": "Collaborative transactions increase everyone's privacy." + }, + "item_3": { + "title": "Optional: <1>Lock funds in a fidelity bond.", + "description": "A fidelity bond increases your chances of selling liquidity substantially." + }, + "item_4": { + "title": "<0>Earn yield by providing liquidity.", + "description": "Offer your sats to the marketplace. No trust or custody required—you are always in full control of your funds." + }, + "item_5": { + "title": "<0>Schedule transactions.", + "description": "Automatically plan and execute a series of collaborative transactions. You can also automatically sweep your funds to cold storage, or use them to open a lightning channel, for example." + }, + "item_6": { + "title": "Go to step one and repeat.", + "description": "Still confused? Dig into the <2>documentation." + } } } diff --git a/src/index.css b/src/index.css index 46bf07f48..5d1805795 100644 --- a/src/index.css +++ b/src/index.css @@ -207,7 +207,7 @@ body, } #mainNav, -.offcanvas-header { +.navbar-offcanvas.offcanvas-header { height: 76px; } @@ -319,8 +319,8 @@ main { border-bottom: 2px solid black; } - .offcanvas, - .offcanvas-backdrop { + .navbar-offcanvas.offcanvas, + .navbar-offcanvas.offcanvas-backdrop { display: none; } } @@ -357,6 +357,59 @@ main { } } +/* Cheatsheet Styles */ +.cheatsheet { + height: auto; + /* page height - navbar height - some spacing*/ + max-height: calc(100vh - 76px - 1rem); + border-top-left-radius: 1rem; + border-top-right-radius: 1rem; + margin: 0px auto; + width: 528px; +} + +.cheatsheet .offcanvas-header .btn-close { + margin-bottom: auto; +} + +.cheatsheet .cheatsheet-list-item { + align-items: start; +} + +.cheatsheet .cheatsheet-list-item.upcoming-feature { + opacity: 0.25; +} + +.cheatsheet .cheatsheet-list-item h6 { + margin-bottom: 0.1rem; +} +.cheatsheet a { + color: inherit; +} + +.cheatsheet .numbered { + display: flex; + justify-content: center; + align-items: center; + min-width: 2rem; + height: 2rem; + border-radius: 50%; + background-color: rgb(0, 0, 0); + color: white; +} + +:root[data-theme='dark'] .cheatsheet .numbered { + color: rgb(0, 0, 0); + background-color: white; +} + +.cheatsheet-link.nav-link { + color: var(--bs-gray-900); +} + +:root[data-theme='dark'] .cheatsheet-link.nav-link { + color: var(--bs-gray-400); +} /* Blurred Text Styles */ .blurred-text { @@ -439,7 +492,8 @@ h2 { color: var(--bs-white); } -.modal-header .btn-close { +.modal-header .btn-close, +:root[data-theme='dark'] .offcanvas-header .btn-close { background: transparent url('data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27 fill=%27%23fff%27%3e%3cpath d=%27M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z%27/%3e%3c/svg%3e') center/1em auto no-repeat;