diff --git a/mobile/app/Home.tsx b/mobile/app/Home.tsx
index a3073532..f26c9581 100644
--- a/mobile/app/Home.tsx
+++ b/mobile/app/Home.tsx
@@ -11,6 +11,7 @@ import { scheduleOnRN } from 'react-native-worklets';
import ActionButtons, { Action } from '@/components/ActionButtons';
import BackupWarning from '@/components/BackupWarning';
import Balance from '@/components/Balance';
+import ReceiveCta from '@/components/ReceiveCta';
import DashboardTiles, { LayerCard } from '@/components/DashboardTiles';
import { McpAgentDashboard } from '@/src/features/mcp/components/McpAgentDashboard';
import { McpTunnelStatusRow } from '@/src/features/mcp/components/McpTunnelStatusRow';
@@ -40,6 +41,7 @@ import { sleep } from '@shared/modules/sleep';
import { capitalizeFirstLetter } from '@shared/modules/string-utils';
import { CommonTransaction } from '@shared/types/common-transaction';
import { NETWORK_ARK, NETWORK_LIGHTNING_TESTNET, NETWORK_LIQUID, NETWORK_LIQUID_TESTNET, NETWORK_SPARK } from '@shared/types/networks';
+import { STORAGE_KEY_RECEIVE_CTA } from '@shared/types/IStorage';
import { CachedTokenInfo } from '@shared/types/token-info';
import { YieldBearingCachedTokenInfo } from '@shared/hooks/useYieldDiscovery';
import { OnrampProps } from './Onramp';
@@ -78,6 +80,18 @@ export default function Home() {
const settingsContext = useSettings();
const hasBackedUpSeed = settingsContext.settings.seedBackedUp === 'ON';
+ // New-user "receive bitcoin" CTA — flag set only on wallet creation, cleared on dismiss/Receive.
+ const [showReceiveCta, setShowReceiveCta] = useState(false);
+
+ useEffect(() => {
+ LayerzStorage.getItem(STORAGE_KEY_RECEIVE_CTA).then((value) => setShowReceiveCta(!!value));
+ }, []);
+
+ const dismissReceiveCta = useCallback(() => {
+ setShowReceiveCta(false);
+ LayerzStorage.setItem(STORAGE_KEY_RECEIVE_CTA, '');
+ }, []);
+
const handleFund = useCallback(() => {
BackgroundExecutor.getAddress(network, accountNumber).then((address) => {
const onrampParams: OnrampProps = { address, network };
@@ -366,7 +380,10 @@ export default function Home() {
{accountNumber === MCP_BALANCE_ACCOUNT_NUMBER ? : null}
{/* Action Buttons Section (hidden on MCP automation account) */}
- {accountNumber !== MCP_BALANCE_ACCOUNT_NUMBER ? : null}
+ {accountNumber !== MCP_BALANCE_ACCOUNT_NUMBER ? : null}
+
+ {/* New-user "receive bitcoin" CTA — caption below the action buttons */}
+ {showReceiveCta && accountNumber !== MCP_BALANCE_ACCOUNT_NUMBER ? : null}
{/* Seed Backup Warning */}
{hasBackedUpSeed === false && }
diff --git a/mobile/components/ActionButtons.tsx b/mobile/components/ActionButtons.tsx
index e5d35f47..24116c7c 100644
--- a/mobile/components/ActionButtons.tsx
+++ b/mobile/components/ActionButtons.tsx
@@ -27,9 +27,13 @@ export const Action = ({ network, text }: { network?: Networks; text: string })
interface ActionButtonsProps {
onFundPress: () => void;
+ /** When true, draws a breathing glow on the Receive button (new-user CTA). */
+ highlightReceive?: boolean;
+ /** Called when the user engages the Receive button — used to dismiss the new-user CTA. */
+ onReceivePress?: () => void;
}
-export default function ActionButtons({ onFundPress }: ActionButtonsProps) {
+export default function ActionButtons({ onFundPress, highlightReceive = false, onReceivePress }: ActionButtonsProps) {
const router = useRouter();
const { network } = useContext(NetworkContext);
@@ -47,6 +51,7 @@ export default function ActionButtons({ onFundPress }: ActionButtonsProps) {
};
const handleReceive = () => {
+ onReceivePress?.();
router.push('/Receive');
};
@@ -55,6 +60,7 @@ export default function ActionButtons({ onFundPress }: ActionButtonsProps) {
};
const handleReceiveOnLightningAddress = () => {
+ onReceivePress?.();
router.push('/ReceiveOnLightningAddress');
};
@@ -122,18 +128,18 @@ export default function ActionButtons({ onFundPress }: ActionButtonsProps) {
const renderReceiveButton = () => {
if (network === NETWORK_LIGHTNING || network === NETWORK_LIGHTNING_TESTNET) {
// Default to Lightning Address receive on tap (as per master behavior)
- return ;
+ return ;
}
if (network === NETWORK_USDT) {
return (
- {}} testID="ReceiveButton" />
+ onReceivePress?.()} glow={highlightReceive} testID="ReceiveButton" />
);
}
- return ;
+ return ;
};
// Render Fund button (only if canBuyWithFiat is true)
diff --git a/mobile/components/HomeActionButton.tsx b/mobile/components/HomeActionButton.tsx
index f07e9190..1037971e 100644
--- a/mobile/components/HomeActionButton.tsx
+++ b/mobile/components/HomeActionButton.tsx
@@ -1,9 +1,17 @@
import { Ionicons, MaterialIcons } from '@expo/vector-icons';
-import React from 'react';
+import React, { useEffect } from 'react';
import { ActivityIndicator, StyleProp, StyleSheet, TextStyle, View, ViewStyle } from 'react-native';
+import Animated, { cancelAnimation, Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
import Pressable, { PressableProps } from './Pressable';
import { ThemedText } from './ThemedText';
+// Bitcoin orange — warm accent that stays visible on every network background gradient.
+const GLOW_COLOR = '#F7931A';
+// Breathing glow opacity range — floors above 0 so the halo stays continuously visible.
+const GLOW_MIN = 0.3;
+const GLOW_MAX = 0.62;
+const GLOW_DURATION = 2400; // ms per half-cycle — slow, calm breathing
+
type IconConfig =
| { name: React.ComponentProps['name']; type: 'material'; size?: number }
| { name: React.ComponentProps['name']; type?: 'ionicons'; size?: number };
@@ -14,9 +22,39 @@ export interface HomeActionButtonProps extends PressableProps {
variant?: 'light' | 'dark';
loading?: boolean;
textStyle?: TextStyle;
+ /** When true, renders a soft breathing glow halo behind the button to draw attention. */
+ glow?: boolean;
}
-export default function HomeActionButton({ title, icon, onPress, variant = 'light', disabled = false, loading = false, style, textStyle, activeOpacity = 0.8, ...restProps }: HomeActionButtonProps) {
+export default function HomeActionButton({
+ title,
+ icon,
+ onPress,
+ variant = 'light',
+ disabled = false,
+ loading = false,
+ style,
+ textStyle,
+ activeOpacity = 0.8,
+ glow = false,
+ ...restProps
+}: HomeActionButtonProps) {
+ const glowOpacity = useSharedValue(0);
+
+ useEffect(() => {
+ if (glow) {
+ // Start dim, then repeat reversed so opacity oscillates GLOW_MIN <-> GLOW_MAX forever.
+ glowOpacity.value = GLOW_MIN;
+ glowOpacity.value = withRepeat(withTiming(GLOW_MAX, { duration: GLOW_DURATION, easing: Easing.inOut(Easing.ease) }), -1, true);
+ } else {
+ cancelAnimation(glowOpacity);
+ glowOpacity.value = withTiming(0, { duration: 300 });
+ }
+ return () => cancelAnimation(glowOpacity);
+ }, [glow, glowOpacity]);
+
+ const glowStyle = useAnimatedStyle(() => ({ opacity: glowOpacity.value }));
+
const getButtonStyle = (): StyleProp => {
const baseStyle: ViewStyle[] = [styles.button];
@@ -66,6 +104,7 @@ export default function HomeActionButton({ title, icon, onPress, variant = 'ligh
return (
+ {glow ? : null}
{renderContent()}
@@ -88,6 +127,20 @@ const styles = StyleSheet.create({
buttonWrapper: {
width: '100%',
},
+ glow: {
+ position: 'absolute',
+ top: -9,
+ left: -9,
+ right: -9,
+ bottom: -9,
+ borderRadius: 36,
+ backgroundColor: GLOW_COLOR,
+ shadowColor: GLOW_COLOR,
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.9,
+ shadowRadius: 14,
+ elevation: 10,
+ },
button: {
width: '100%',
height: 54,
diff --git a/mobile/components/ReceiveCta.tsx b/mobile/components/ReceiveCta.tsx
new file mode 100644
index 00000000..7cd8de55
--- /dev/null
+++ b/mobile/components/ReceiveCta.tsx
@@ -0,0 +1,76 @@
+import { Ionicons } from '@expo/vector-icons';
+import React from 'react';
+import { StyleSheet, View } from 'react-native';
+import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
+import { scheduleOnRN } from 'react-native-worklets';
+
+import Pressable from '@/components/Pressable';
+import { ThemedText } from '@/components/ThemedText';
+
+export interface ReceiveCtaProps {
+ onDismiss: () => void;
+}
+
+// Bitcoin orange — keep in sync with the Receive-button glow in HomeActionButton.
+const ACCENT = '#F7931A';
+const FADE_OUT_DURATION = 260;
+
+// One-time CTA nudging brand-new (created, not imported) wallets to receive their first bitcoin.
+const ReceiveCta: React.FC = ({ onDismiss }) => {
+ const opacity = useSharedValue(1);
+ const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
+
+ // Fade out first, then notify the parent to remove it and persist the dismissed flag.
+ const handleDismiss = () => {
+ opacity.value = withTiming(0, { duration: FADE_OUT_DURATION }, (finished) => {
+ if (finished) {
+ scheduleOnRN(onDismiss);
+ }
+ });
+ };
+
+ return (
+
+
+
+ Start by receiving bitcoin
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ wrapper: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginTop: -18,
+ marginBottom: 24,
+ },
+ row: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ },
+ arrow: {
+ marginTop: 1,
+ },
+ text: {
+ fontSize: 14,
+ fontWeight: '500',
+ color: 'rgba(255, 255, 255, 0.9)',
+ },
+ closeButton: {
+ position: 'absolute',
+ right: 0,
+ width: 24,
+ height: 24,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
+
+export default ReceiveCta;
diff --git a/mobile/src/modules/background-executor.ts b/mobile/src/modules/background-executor.ts
index d25a5eea..8d590f9d 100644
--- a/mobile/src/modules/background-executor.ts
+++ b/mobile/src/modules/background-executor.ts
@@ -18,7 +18,15 @@ import {
clearWalletCache,
} from '@shared/modules/wallet-utils';
import { IBackgroundCaller, OpenPopupRequest } from '@shared/types/IBackgroundCaller';
-import { ENCRYPTED_PREFIX, STORAGE_KEY_ACCEPTED_TOS, STORAGE_KEY_EVM_XPUB, STORAGE_KEY_MNEMONIC, STORAGE_KEY_WHITELIST, STORAGE_KEY_SEED_VERIFIED } from '@shared/types/IStorage';
+import {
+ ENCRYPTED_PREFIX,
+ STORAGE_KEY_ACCEPTED_TOS,
+ STORAGE_KEY_EVM_XPUB,
+ STORAGE_KEY_MNEMONIC,
+ STORAGE_KEY_RECEIVE_CTA,
+ STORAGE_KEY_WHITELIST,
+ STORAGE_KEY_SEED_VERIFIED,
+} from '@shared/types/IStorage';
import { Networks, NETWORK_ARK, NETWORK_ARK_MUTINYNET, NETWORK_BITCOIN, NETWORK_LIQUID, NETWORK_LIQUID_TESTNET, NETWORK_SPARK, NETWORK_STACKS } from '@shared/types/networks';
import { BrowserBridge } from '../class/browser-bridge';
import { LayerzStorage } from '../class/layerz-storage';
@@ -163,6 +171,8 @@ export const BackgroundExecutor: IBackgroundCaller = {
await LayerzStorage.setItem(STORAGE_KEY_EVM_XPUB, xpub);
await LayerzStorage.setItem(STORAGE_KEY_EVM_XPUB, xpub);
await saveBitcoinXpubs(LayerzStorage, mnemonic);
+ // Freshly created (not imported) wallet: flag the home screen to show the one-time "receive bitcoin" CTA.
+ await LayerzStorage.setItem(STORAGE_KEY_RECEIVE_CTA, 'true');
return { mnemonic };
},
diff --git a/shared/types/IStorage.ts b/shared/types/IStorage.ts
index 2d589838..5ba59b84 100644
--- a/shared/types/IStorage.ts
+++ b/shared/types/IStorage.ts
@@ -14,6 +14,7 @@ export const STORAGE_KEY_SYMBIOSIS_TRANSFERS = 'STORAGE_KEY_SYMBIOSIS_TRANSFERS'
export const STORAGE_KEY_FLASHNET_TRANSFERS = 'STORAGE_KEY_FLASHNET_TRANSFERS';
export const STORAGE_KEY_SPARK_REFUNDED_DEPOSITS = 'STORAGE_KEY_SPARK_REFUNDED_DEPOSITS';
export const STORAGE_KEY_SPARK_LN_INVOICE_IDS = 'STORAGE_KEY_SPARK_LN_INVOICE_IDS';
+export const STORAGE_KEY_RECEIVE_CTA = 'STORAGE_KEY_RECEIVE_CTA';
export interface IStorage {
setItem(key: string, value: string): Promise;