diff --git a/CHANGELOG.md b/CHANGELOG.md index 12fe47fb7f1..4314519fa91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - added: Reverse-resolve recipient addresses to ENS / Unstoppable Domains / ZNS names in the send flow, address modal, and transaction history. - changed: Migrate Monero to the react-native-monero implementation, replacing edge-currency-monero - changed: Migrate package manager from yarn to npm. +- changed: Deprecate Botanix by switching it to keys-only mode on July 9, 2026. - fixed: Android build failure from the home screen long-press shortcuts feature, caused by an expo-quick-actions Kotlin compile error under Kotlin 2.3. ## 4.48.2 (2026-06-03) diff --git a/src/__tests__/constants/WalletAndCurrencyConstants.test.ts b/src/__tests__/constants/WalletAndCurrencyConstants.test.ts new file mode 100644 index 00000000000..24cff6f4a64 --- /dev/null +++ b/src/__tests__/constants/WalletAndCurrencyConstants.test.ts @@ -0,0 +1,49 @@ +import { afterEach, describe, expect, jest, test } from '@jest/globals' + +import { + isKeysOnlyModeDate, + SPECIAL_CURRENCY_INFO +} from '../../constants/WalletAndCurrencyConstants' + +const DEPRECATION_MS = new Date('2026-07-09T00:00:00.000Z').getTime() + +describe('isKeysOnlyModeDate', function () { + afterEach(() => { + jest.restoreAllMocks() + }) + + const date = new Date('2026-07-09T00:00:00.000Z') + + test('returns false before the date', function () { + jest.spyOn(Date, 'now').mockReturnValue(DEPRECATION_MS - 1) + expect(isKeysOnlyModeDate(date)).toBe(false) + }) + + test('returns true exactly on the date', function () { + jest.spyOn(Date, 'now').mockReturnValue(DEPRECATION_MS) + expect(isKeysOnlyModeDate(date)).toBe(true) + }) + + test('returns true after the date', function () { + jest.spyOn(Date, 'now').mockReturnValue(DEPRECATION_MS + 86400000) + expect(isKeysOnlyModeDate(date)).toBe(true) + }) +}) + +describe('botanix keysOnlyMode', function () { + afterEach(() => { + jest.restoreAllMocks() + }) + + // The flag is a getter so it re-evaluates the date on each read, rather than + // freezing the value at module load. + test('re-evaluates the date on each read', function () { + const nowSpy = jest.spyOn(Date, 'now') + + nowSpy.mockReturnValue(DEPRECATION_MS - 1) + expect(SPECIAL_CURRENCY_INFO.botanix.keysOnlyMode).toBe(false) + + nowSpy.mockReturnValue(DEPRECATION_MS) + expect(SPECIAL_CURRENCY_INFO.botanix.keysOnlyMode).toBe(true) + }) +}) diff --git a/src/components/modals/WalletListModal.tsx b/src/components/modals/WalletListModal.tsx index 5e5dea7a5ea..86d655ce338 100644 --- a/src/components/modals/WalletListModal.tsx +++ b/src/components/modals/WalletListModal.tsx @@ -86,13 +86,6 @@ interface Props { parentWalletId?: string } -const keysOnlyModeAssets: EdgeAsset[] = Object.keys(SPECIAL_CURRENCY_INFO) - .filter(pluginId => isKeysOnlyPlugin(pluginId)) - .map(pluginId => ({ - pluginId, - tokenId: null - })) - export const WalletListModal: React.FC = props => { const { bridge, @@ -140,10 +133,16 @@ export const WalletListModal: React.FC = props => { // #region Init - // Prevent plugins that are "watch only" from being used unless it's explicitly allowed + // Prevent plugins that are "watch only" from being used unless it's explicitly allowed. + // Computed per render (not once at import) so date-gated keysOnlyMode plugins are + // honored mid-session without an app restart. const walletListExcludeAssets = React.useMemo(() => { const result = excludeAssets - return allowKeysOnlyMode ? result : keysOnlyModeAssets.concat(result ?? []) + if (allowKeysOnlyMode) return result + const keysOnlyModeAssets: EdgeAsset[] = Object.keys(SPECIAL_CURRENCY_INFO) + .filter(pluginId => isKeysOnlyPlugin(pluginId)) + .map(pluginId => ({ pluginId, tokenId: null })) + return keysOnlyModeAssets.concat(result ?? []) }, [allowKeysOnlyMode, excludeAssets]) // #endregion Init diff --git a/src/constants/WalletAndCurrencyConstants.ts b/src/constants/WalletAndCurrencyConstants.ts index 41d623e4a22..f288554d3e2 100644 --- a/src/constants/WalletAndCurrencyConstants.ts +++ b/src/constants/WalletAndCurrencyConstants.ts @@ -609,6 +609,12 @@ export const SPECIAL_CURRENCY_INFO: Record = { showChainIcon: true, dummyPublicAddress: '0x0d73358506663d484945ba85d0cd435ad610b0a0', isImportKeySupported: true, + // Getter, not a fixed boolean: this is read on each access (e.g. via + // isKeysOnlyPlugin) so the date gate re-evaluates during a running session + // and takes effect at the cutover without requiring an app restart. + get keysOnlyMode(): boolean { + return isKeysOnlyModeDate(new Date('2026-07-09T00:00:00.000Z')) + }, walletConnectV2ChainId: { namespace: 'eip155', reference: '3637' @@ -1094,6 +1100,25 @@ function isZecBroken(): boolean { return false } +/** + * Generic time-gate for deprecating an asset into keysOnlyMode (watch-only) on + * a specific date. Returns true once the current time is on or after `date`. On + * and after the date the asset becomes keys-only: existing wallets remain + * accessible (keys-only) but no new wallets can be created. + * + * Not specific to any single plugin. Call it from a `keysOnlyMode` getter so + * the gate re-evaluates on each read and takes effect at the cutover without an + * app restart: + * get keysOnlyMode(): boolean { return isKeysOnlyModeDate(new Date('YYYY-MM-DD')) } + * + * Declared as a hoisted function (not a `const`) because `SPECIAL_CURRENCY_INFO` + * references it during module initialization, before a later `const` would be + * assigned (temporal dead zone). + */ +export function isKeysOnlyModeDate(date: Date): boolean { + return Date.now() >= date.getTime() +} + export const USD_FIAT = 'iso:USD' /** * Get the fiat symbol from an iso:[fiat] OR fiat currency code