Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
54a9ffd
frontend: add config context and fetch config once on startup
smokyisthatyou Mar 4, 2026
5cd1bf7
frontend: keep guide hidden by default on first run
smokyisthatyou Apr 13, 2026
2992d26
frontend: type frontend config keys in TConfig
smokyisthatyou Apr 8, 2026
6f8a260
frontend: type backend config and add typed config updates
smokyisthatyou Apr 8, 2026
4d19d28
frontend: complete TConfig typing
smokyisthatyou May 14, 2026
7642796
frontend: migrate NewBadge to useConfig
smokyisthatyou May 14, 2026
3f739db
frontend: remove duplicate onChangeConfig writes in advanced settings
smokyisthatyou May 14, 2026
06fcb73
frontend: await bitsurance setConfig when clearing cancellation flag
smokyisthatyou May 15, 2026
1e34610
frontend: address minor nits
smokyisthatyou May 15, 2026
21682d8
fix: remove unused import
smokyisthatyou May 19, 2026
24e6146
fix: export only config types that are imported in other files
smokyisthatyou May 20, 2026
fd60ec8
nit: add comment for deprecated coisn to match backend
smokyisthatyou May 20, 2026
c5b65e0
fix: drop redundant frontend/backend guards after config loads
smokyisthatyou May 20, 2026
4a1496f
fix: drop redundant Boolean() on boolean config fields
smokyisthatyou May 20, 2026
b5dddcf
frontend: use useConfig in swap component
smokyisthatyou May 20, 2026
9c56d5d
fix: move TConfigUpdate types to utils/config
smokyisthatyou May 20, 2026
d9f38ee
fix: type api getConfig as Promise<TConfig>
smokyisthatyou May 20, 2026
ca0308b
fix: prefix config types with TConfig
smokyisthatyou May 20, 2026
d0eb1d8
fix: drop config normalize-on-read
smokyisthatyou May 20, 2026
bb9cdc0
fix: remove cast utils setConfig merge
smokyisthatyou May 20, 2026
f30f894
fix: drop mock-config; inline TConfig casts in tests
smokyisthatyou May 20, 2026
4037024
fix: read gap limit config from useConfig in CustomGapLimitSettings
smokyisthatyou May 20, 2026
aa6a5bf
fix: read auth setting from useConfig in EnableAuthSetting
smokyisthatyou May 20, 2026
c34305e
fix: stabilize export-logs Android e2e after config provider
smokyisthatyou May 20, 2026
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
19 changes: 19 additions & 0 deletions frontends/mobiletests/e2e/export-logs-save.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ const getSaveDialogElement = async (driver) => {
return null;
};

/** Dismiss the in-app guide overlay if it is open (blocks clicks on settings). */
const dismissGuideIfPresent = async (driver) => {
await driver.execute(() => {
const overlays = document.querySelectorAll('div');
for (const el of overlays) {
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.

Please use data-testid on the dialog component's overlay so you dont have to traverse the DOM here.

The react testing library style would more be to find web accessible elements by role atc. https://testing-library.com/docs/queries/about/#priority
but refactoring the diaog/overlay seems a bit out of scope of this PR so best to just add data-testid in this dialog.

const cls = el.className;
if (typeof cls === 'string' && cls.includes('overlay') && cls.includes('show')) {
el.click();
return;
}
}
});
};

describe('Export logs uses save dialog', function () {
this.timeout(180000);

Expand Down Expand Up @@ -56,8 +70,13 @@ describe('Export logs uses save dialog', function () {

const exportLogsButton = await driver.$('//button[contains(., "Export logs")]');
await exportLogsButton.waitForDisplayed({ timeout: 60000 });
await dismissGuideIfPresent(driver);
await exportLogsButton.waitForClickable({ timeout: 10000 });
await exportLogsButton.click();

// Allow native save picker to open after the export-log API call.
await driver.pause(3000);

await driver.switchContext('NATIVE_APP');
const saveDialog = await getSaveDialogElement(driver);
expect(saveDialog, 'expected Android save dialog to be visible').to.not.equal(null);
Expand Down
129 changes: 129 additions & 0 deletions frontends/web/src/api/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: Apache-2.0

import { apiGet, apiPost } from '@/utils/request';
import type { Fiat } from '@/api/account';
import type { BtcUnit } from '@/api/coins';

type TElectrumServerInfo = Readonly<{
server: string;
tls: boolean;
pemCert: string;
}>;

type TBtcCoinConfig = Readonly<{
electrumServers: TElectrumServerInfo[];
}>;

/** BTC-based coin keys in backend config (see backend/config/config.go). */
export type TConfigBackendBtcCoinKey = 'btc' | 'tbtc' | 'ltc' | 'tltc';

// Mirrors backend/config/config.go ethCoinConfig: DeprecatedActiveERC20Tokens (JSON key
// "activeERC20Tokens"). Deprecated — ERC20 activation is per-account in accounts config; kept
// for migration / compatibility with persisted app config.
type TEthCoinConfig = Readonly<{
activeERC20Tokens: string[];
Comment thread
thisconnect marked this conversation as resolved.
}>;

export type TConfigBackendProxy = Readonly<{
useProxy: boolean;
proxyAddress: string;
}>;

/** Keys used by NewBadge to mark UI elements as seen. */
export type TConfigFrontendBadgeKey =
| 'hasSeenMarketplaceNudge'
| 'hasSeenSwapMarketTab'
| 'hasSeenOtcMarketTab';

/** Dynamic frontend keys written when dismissing Status banners. */
type TConfigFrontendDismissibleDynamicKey =
| `update-${string}`
| `banner-backup-${string}`
| `banner-${string}-${string}`;

/** Known static frontend keys written when dismissing Status banners. */
type TConfigFrontendDismissibleKnownKey =
| 'walletConnectDisclaimerDismissed'
| 'skipTestingWarning'
| 'mobile-data-warning';

export type TConfigFrontendDismissibleKey =
| TConfigFrontendDismissibleKnownKey
| TConfigFrontendDismissibleDynamicKey;

export type TConfigFrontend = Readonly<{
guideShown?: boolean;
hideAmounts?: boolean;
darkmode?: boolean;
expertFee?: boolean;
coinControl?: boolean;
selectedExchangeRegion?: string;
hideEnableRememberWalletDialog?: boolean;
hasUsedWalletConnect?: boolean;
bitsuranceNotifyCancellation?: string[];

hasSeenMarketplaceNudge?: boolean;
hasSeenSwapMarketTab?: boolean;
hasSeenOtcMarketTab?: boolean;

skipBitrefillWidgetDisclaimer?: boolean;
skipBTCDirectWidgetDisclaimer?: boolean;
skipBTCDirectOTCDisclaimer?: boolean;
skipMoonpayDisclaimer?: boolean;
skipPocketDisclaimer?: boolean;
skipPocketOTCDisclaimer?: boolean;
skipBitsuranceDisclaimer?: boolean;
skipSwapkitDisclaimer?: boolean;

walletConnectDisclaimerDismissed?: boolean;
skipTestingWarning?: boolean;
'mobile-data-warning'?: boolean;
}> & Readonly<{
[key in TConfigFrontendDismissibleDynamicKey]?: boolean;
}>;

export type TConfigBackend = Readonly<{
proxy: TConfigBackendProxy;
/**
* Deprecated global coin activation flags (backend/config/config.go: DeprecatedBitcoinActive,
* DeprecatedLitecoinActive, DeprecatedEthereumActive). Coins are configured per account now;
* kept for migration / compatibility with persisted app config.
*/
bitcoinActive: boolean;
litecoinActive: boolean;
ethereumActive: boolean;
authentication: boolean;
btc: TBtcCoinConfig;
tbtc: TBtcCoinConfig;
rbtc: TBtcCoinConfig;
ltc: TBtcCoinConfig;
tltc: TBtcCoinConfig;
eth: TEthCoinConfig;
teth: Record<string, never>;
reth: Record<string, never>;
fiatList: Fiat[];
mainFiat: Fiat;
userLanguage: string;
btcUnit: BtcUnit;
startInTestnet: boolean;
gapLimitReceive: number;
gapLimitChange: number;
}>;

export type TConfig = {
readonly backend: TConfigBackend;
readonly frontend: TConfigFrontend;
};
Comment thread
thisconnect marked this conversation as resolved.

/**
* Fetch config from the backend (see handlers.getAppConfig).
* Use setConfig from @/utils/config for partial merge-on-write updates.
*/
export const getConfig = (): Promise<TConfig> => apiGet('config');

/**
* Post a config object to the backend.
*/
export const setConfig = (config: TConfig): Promise<void> => {
return apiPost('config', config);
};
Comment thread
thisconnect marked this conversation as resolved.
Comment thread
thisconnect marked this conversation as resolved.
11 changes: 3 additions & 8 deletions frontends/web/src/components/banners/offline-error.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// SPDX-License-Identifier: Apache-2.0

import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Message } from '../message/message';
import { getConfig } from '@/utils/config';
import { useConfig } from '@/contexts/ConfigProvider';
import style from './offline-errors.module.css';

type Props = {
Expand All @@ -13,13 +12,9 @@ type Props = {
export const OfflineError = ({
error,
}: Props) => {

const { t } = useTranslation();
const [usesProxy, setUsesProxy] = useState<boolean>();

useEffect(() => {
getConfig().then(({ backend }) => setUsesProxy(backend.proxy.useProxy));
}, []);
const { config } = useConfig();
const usesProxy = config?.backend.proxy?.useProxy ?? false;

// Status: offline error
const offlineErrorTextLines: string[] = [];
Expand Down
15 changes: 6 additions & 9 deletions frontends/web/src/components/new-badge/new-badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@

import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { TConfigFrontendBadgeKey } from '@/api/config';
import { Badge } from '@/components/badge/badge';
import { useLoad } from '@/hooks/api';
import { getConfig, setConfig } from '@/utils/config';

type TConfigKey = 'hasSeenMarketplaceNudge' | 'hasSeenSwapMarketTab' | 'hasSeenOtcMarketTab';
import { useConfig } from '@/contexts/ConfigProvider';

type TProps = {
className?: string;
configKey: TConfigKey;
configKey: TConfigFrontendBadgeKey;
hideOnPathPrefix?: string;
markAsSeen?: boolean;
pathname?: string;
Expand All @@ -28,7 +26,7 @@ export const NewBadge = ({
type = 'new',
}: TProps) => {
const { t } = useTranslation();
const config = useLoad(getConfig);
const { config, setConfig } = useConfig();
const [showBadge, setShowBadge] = useState<boolean | undefined>(undefined);

const persistAsSeen = useCallback(() => {
Expand All @@ -37,14 +35,13 @@ export const NewBadge = ({
}
setShowBadge(false);
setConfig({ frontend: { [configKey]: true } });
}, [configKey, showBadge]);
}, [configKey, setConfig, showBadge]);

useEffect(() => {
if (!config) {
return;
}
const frontendConfig = config.frontend as Record<string, unknown> | undefined;
const hasSeenBadge = Boolean(frontendConfig?.[configKey]);
const hasSeenBadge = config.frontend[configKey];
setShowBadge(currentShowBadge => (
currentShowBadge === false ? false : !hasSeenBadge
));
Expand Down
30 changes: 12 additions & 18 deletions frontends/web/src/components/status/status.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: Apache-2.0

import { ReactNode, useCallback, useEffect, useState } from 'react';
import { getConfig, setConfig } from '@/utils/config';
import { ReactNode } from 'react';
import type { TConfigFrontendDismissibleKey } from '@/api/config';
import { useConfig } from '@/contexts/ConfigProvider';
import { CloseXDark, CloseXWhite } from '@/components/icon';
import { useDarkmode } from '@/hooks/darkmode';
import { Message } from '@/components/message/message';
Expand All @@ -15,7 +16,7 @@ type TProps = {
// used as keyName in the config if dismissing the status should be persisted, so it is not
// shown again. Use an empty string if it should be dismissible without storing it in the
// config, so the status will be shown again the next time.
dismissibleKey: string;
dismissibleKey: TConfigFrontendDismissibleKey | '';
className?: string;
children: ReactNode;
};
Expand All @@ -28,21 +29,15 @@ export const Status = ({
className = '',
children,
}: TProps) => {
// note: dismissible can be falsy i.e. empty string ''
const [show, setShow] = useState(dismissibleKey ? false : true);

const { config, setConfig } = useConfig();
const { isDarkMode } = useDarkmode();

const checkConfig = useCallback(async () => {
if (dismissibleKey) {
const config = await getConfig();
setShow(!config ? true : !config.frontend[dismissibleKey]);
}
}, [dismissibleKey]);

useEffect(() => {
checkConfig();
}, [checkConfig]);
// note: dismissibleKey can be falsy i.e. empty string ''
const show = hidden
? false
: dismissibleKey
? (config ? !config.frontend[dismissibleKey] : true)
: true;

const dismiss = async () => {
if (!dismissibleKey) {
Expand All @@ -53,10 +48,9 @@ export const Status = ({
[dismissibleKey]: true,
}
});
setShow(false);
};

if (hidden || !show) {
if (!show) {
return null;
}

Expand Down
10 changes: 5 additions & 5 deletions frontends/web/src/components/terms/bitrefill-terms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { TAccount } from '@/api/account';
import { isBitcoinOnly } from '@/routes/account/utils';
import { getBitrefillHelpLink, getBitrefillLimitsLink } from '@/routes/market/bitrefill-guide';
import { Button, Checkbox } from '@/components/forms';
import { setConfig } from '@/utils/config';
import { useConfig } from '@/contexts/ConfigProvider';
import { A } from '../anchor/anchor';
import style from './terms.module.css';

Expand All @@ -32,12 +32,12 @@ export const getBitrefillPrivacyLink = () => {
return 'https://www.bitrefill.com/privacy/?hl=' + hl;
};

const handleSkipDisclaimer = (e: ChangeEvent<HTMLInputElement>) => {
setConfig({ frontend: { skipBitrefillWidgetDisclaimer: e.target.checked } });
};

export const BitrefillTerms = ({ account, onAgreedTerms }: TProps) => {
const { t } = useTranslation();
const { setConfig } = useConfig();
const handleSkipDisclaimer = (e: ChangeEvent<HTMLInputElement>) => {
setConfig({ frontend: { skipBitrefillWidgetDisclaimer: e.target.checked } });
};
Comment thread
thisconnect marked this conversation as resolved.

const isBitcoin = isBitcoinOnly(account.coinCode);
return (
Expand Down
3 changes: 2 additions & 1 deletion frontends/web/src/components/terms/bitsurance-terms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useTranslation } from 'react-i18next';
import { ChangeEvent } from 'react';
import { Button, Checkbox } from '@/components/forms';
import { setConfig } from '@/utils/config';
import { useConfig } from '@/contexts/ConfigProvider';
import { A } from '@/components/anchor/anchor';
import style from './terms.module.css';
import { i18n } from '@/i18n/i18n';
Expand All @@ -14,6 +14,7 @@ type TProps = {

export const BitsuranceTerms = ({ onAgreedTerms }: TProps) => {
const { t } = useTranslation();
const { setConfig } = useConfig();
const handleSkipDisclaimer = (e: ChangeEvent<HTMLInputElement>) => {
setConfig({ frontend: { skipBitsuranceDisclaimer: e.target.checked } });
};
Expand Down
10 changes: 5 additions & 5 deletions frontends/web/src/components/terms/btcdirect-otc-terms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useTranslation } from 'react-i18next';
import { ChangeEvent } from 'react';
import { Button, Checkbox } from '@/components/forms';
import { setConfig } from '@/utils/config';
import { useConfig } from '@/contexts/ConfigProvider';
import { i18n } from '@/i18n/i18n';
import { A } from '../anchor/anchor';
import style from './terms.module.css';
Expand All @@ -27,12 +27,12 @@ export const getBTCDirectPrivacyLink = () => {
}
};

const handleSkipDisclaimer = (e: ChangeEvent<HTMLInputElement>) => {
setConfig({ frontend: { skipBTCDirectOTCDisclaimer: e.target.checked } });
};

export const BTCDirectOTCTerms = ({ onContinue }: TProps) => {
const { t } = useTranslation();
const { setConfig } = useConfig();
const handleSkipDisclaimer = (e: ChangeEvent<HTMLInputElement>) => {
setConfig({ frontend: { skipBTCDirectOTCDisclaimer: e.target.checked } });
};
Comment thread
thisconnect marked this conversation as resolved.

return (
<div className={style.disclaimerContainer}>
Expand Down
10 changes: 5 additions & 5 deletions frontends/web/src/components/terms/btcdirect-terms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { isBitcoinOnly } from '@/routes/account/utils';
import { Button, Checkbox } from '@/components/forms';
import { setConfig } from '@/utils/config';
import { useConfig } from '@/contexts/ConfigProvider';
import { TAccount } from '@/api/account';
import { A } from '@/components/anchor/anchor';
import { getBTCDirectAboutUsLink } from '@/routes/market/components/infocontent';
Expand All @@ -16,12 +16,12 @@ type TProps = {
onAgreedTerms: () => void;
};

const handleSkipDisclaimer = (e: ChangeEvent<HTMLInputElement>) => {
setConfig({ frontend: { skipBTCDirectWidgetDisclaimer: e.target.checked } });
};

export const BTCDirectTerms = ({ account, onAgreedTerms }: TProps) => {
const { t } = useTranslation();
const { setConfig } = useConfig();
const handleSkipDisclaimer = (e: ChangeEvent<HTMLInputElement>) => {
setConfig({ frontend: { skipBTCDirectWidgetDisclaimer: e.target.checked } });
};

const isBitcoin = isBitcoinOnly(account.coinCode);

Expand Down
Loading