diff --git a/app/mobile/app/_layout.tsx b/app/mobile/app/_layout.tsx
index 3748c20..5b374bd 100644
--- a/app/mobile/app/_layout.tsx
+++ b/app/mobile/app/_layout.tsx
@@ -7,7 +7,7 @@ import * as Linking from "expo-linking";
import { Stack, useRouter } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useEffect } from "react";
-import { useColorScheme } from "react-native";
+import { ThemeProvider as AppThemeProvider, useAppTheme } from "@/context/ThemeContext";
// Ensure web build or Expo web uses the local backend during development
if (typeof document !== "undefined" && !(global as any).API_BASE_URL) {
// Expo web typically runs on localhost; ensure the app hits the backend on port 4000
@@ -64,7 +64,15 @@ function DevPoller() {
}
export default function RootLayout() {
- const colorScheme = useColorScheme();
+ return (
+
+
+
+ );
+}
+
+function ThemedNavigationProvider() {
+ const { colorScheme } = useAppTheme();
return (
@@ -81,7 +89,7 @@ export default function RootLayout() {
-
+
);
}
@@ -100,6 +108,8 @@ function AppShell() {
+
+
{isReady && settings.biometricLockEnabled ? (
diff --git a/app/mobile/app/index.tsx b/app/mobile/app/index.tsx
index 58e8b9b..b28850b 100644
--- a/app/mobile/app/index.tsx
+++ b/app/mobile/app/index.tsx
@@ -3,58 +3,62 @@ import React from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import NotificationCenter from "../components/notifications/NotificationCenter";
+import { useAppTheme } from "@/context/ThemeContext";
export default function HomeScreen() {
+ const { colors } = useAppTheme();
+ const s = makeStyles(colors);
+
return (
-
+
{/* Bell */}
-
- QuickEx
+
+ QuickEx
-
+
Fast, privacy-focused payment link platform built on Stellar.
-
- Instant Payments
-
+
+ Instant Payments
+
Receive USDC, XLM, or any Stellar asset directly to your
self-custody wallet.
-
- Scan to Pay
+
+ Scan to Pay
-
- Connect Wallet
+
+ Connect Wallet
-
- Security Settings
+
+ Security Settings
{/* Quick Receive */}
-
- Quick Receive
+
+ Quick Receive
{/* Transaction History */}
-
- Transaction History
+
+ Transaction History
@@ -62,92 +66,94 @@ export default function HomeScreen() {
);
}
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: "#ffffff",
- },
- content: {
- flex: 1,
- padding: 24,
- justifyContent: "center",
- alignItems: "center",
- },
- title: {
- fontSize: 42,
- fontWeight: "bold",
- color: "#000",
- marginBottom: 8,
- },
- subtitle: {
- fontSize: 18,
- color: "#666",
- textAlign: "center",
- marginBottom: 40,
- },
- card: {
- width: "100%",
- padding: 20,
- borderRadius: 12,
- backgroundColor: "#f5f5f5",
- marginBottom: 30,
- },
- cardTitle: {
- fontSize: 20,
- fontWeight: "600",
- color: "#333",
- marginBottom: 8,
- },
- cardText: {
- fontSize: 16,
- color: "#555",
- lineHeight: 22,
- },
+function makeStyles(colors: ReturnType["colors"]) {
+ return StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ content: {
+ flex: 1,
+ padding: 24,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ title: {
+ fontSize: 42,
+ fontWeight: "bold",
+ color: colors.text,
+ marginBottom: 8,
+ },
+ subtitle: {
+ fontSize: 18,
+ color: colors.textSecondary,
+ textAlign: "center",
+ marginBottom: 40,
+ },
+ card: {
+ width: "100%",
+ padding: 20,
+ borderRadius: 12,
+ backgroundColor: colors.card,
+ marginBottom: 30,
+ },
+ cardTitle: {
+ fontSize: 20,
+ fontWeight: "600",
+ color: colors.text,
+ marginBottom: 8,
+ },
+ cardText: {
+ fontSize: 16,
+ color: colors.textSecondary,
+ lineHeight: 22,
+ },
- /* Primary Button */
- primaryButton: {
- backgroundColor: "#000",
- paddingVertical: 16,
- paddingHorizontal: 32,
- borderRadius: 8,
- width: "100%",
- alignItems: "center",
- marginBottom: 12,
- },
- primaryButtonText: {
- color: "#fff",
- fontSize: 18,
- fontWeight: "bold",
- },
- /* Quick Receive Button */
- quickReceiveButton: {
- backgroundColor: "#10B981",
- paddingVertical: 16,
- paddingHorizontal: 32,
- borderRadius: 8,
- width: "100%",
- alignItems: "center",
- marginBottom: 12,
- },
- quickReceiveButtonText: {
- color: "#fff",
- fontSize: 18,
- fontWeight: "600",
- },
+ /* Primary Button */
+ primaryButton: {
+ backgroundColor: colors.primaryBtn,
+ paddingVertical: 16,
+ paddingHorizontal: 32,
+ borderRadius: 8,
+ width: "100%",
+ alignItems: "center",
+ marginBottom: 12,
+ },
+ primaryButtonText: {
+ color: colors.primaryBtnText,
+ fontSize: 18,
+ fontWeight: "bold",
+ },
+ /* Quick Receive Button */
+ quickReceiveButton: {
+ backgroundColor: colors.success,
+ paddingVertical: 16,
+ paddingHorizontal: 32,
+ borderRadius: 8,
+ width: "100%",
+ alignItems: "center",
+ marginBottom: 12,
+ },
+ quickReceiveButtonText: {
+ color: "#fff",
+ fontSize: 18,
+ fontWeight: "600",
+ },
- /* Secondary Button */
- secondaryButton: {
- paddingVertical: 16,
- paddingHorizontal: 32,
- borderRadius: 8,
- width: "100%",
- alignItems: "center",
- borderWidth: 1,
- borderColor: "#000",
- },
- secondaryButtonText: {
- color: "#000",
- fontSize: 18,
- fontWeight: "600",
- },
-});
+ /* Secondary Button */
+ secondaryButton: {
+ paddingVertical: 16,
+ paddingHorizontal: 32,
+ borderRadius: 8,
+ width: "100%",
+ alignItems: "center",
+ borderWidth: 1,
+ borderColor: colors.secondaryBtnBorder,
+ },
+ secondaryButtonText: {
+ color: colors.secondaryBtnText,
+ fontSize: 18,
+ fontWeight: "600",
+ },
+ });
+}
diff --git a/app/mobile/app/payment-confirmation.tsx b/app/mobile/app/payment-confirmation.tsx
index 98071a0..760b8db 100644
--- a/app/mobile/app/payment-confirmation.tsx
+++ b/app/mobile/app/payment-confirmation.tsx
@@ -5,10 +5,14 @@ import { Alert, Pressable, StyleSheet, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useSecurity } from "@/hooks/use-security";
+import { useAppTheme } from "@/context/ThemeContext";
export default function PaymentConfirmationScreen() {
const router = useRouter();
const { authenticateForSensitiveAction } = useSecurity();
+ const { colors } = useAppTheme();
+ const s = makeStyles(colors);
+
const params = useLocalSearchParams<{
username: string;
amount: string;
@@ -48,21 +52,21 @@ export default function PaymentConfirmationScreen() {
if (!isValid) {
return (
-
-
-
- !
- Invalid Payment Link
-
+
+
+
+ !
+ Invalid Payment Link
+
This payment link is missing required information. Please try
scanning again or check the link.
router.replace("/")}
>
- Go Back
+ Go Back
@@ -70,40 +74,40 @@ export default function PaymentConfirmationScreen() {
}
return (
-
-
- Confirm Payment
-
+
+
+ Confirm Payment
+
Review the details below before paying
-
-
-
-
+
+
+
+
{memo ? (
<>
-
-
+
+
>
) : null}
{isPrivate ? (
<>
-
-
+
+
>
) : null}
-
-
- Pay with Wallet
+
+
+ Pay with Wallet
router.replace("/")}
>
- Cancel
+ Cancel
@@ -111,108 +115,114 @@ export default function PaymentConfirmationScreen() {
);
}
+type Styles = ReturnType;
+
function Row({
label,
value,
highlight,
+ s,
}: {
label: string;
value: string;
highlight?: boolean;
+ s: Styles;
}) {
return (
-
- {label}
-
+
+ {label}
+
{value}
);
}
-const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: "#fff" },
- content: {
- flex: 1,
- padding: 24,
- justifyContent: "center",
- },
- heading: {
- fontSize: 32,
- fontWeight: "bold",
- color: "#000",
- marginBottom: 4,
- },
- subheading: {
- fontSize: 16,
- color: "#888",
- marginBottom: 32,
- },
- card: {
- backgroundColor: "#F5F5F5",
- borderRadius: 16,
- padding: 20,
- marginBottom: 40,
- },
- row: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- paddingVertical: 14,
- },
- rowLabel: { fontSize: 15, color: "#888" },
- rowValue: {
- fontSize: 16,
- fontWeight: "500",
- color: "#222",
- flexShrink: 1,
- textAlign: "right",
- },
- rowValueHighlight: { fontSize: 20, fontWeight: "700", color: "#000" },
- divider: { height: 1, backgroundColor: "#E5E5E5" },
- actions: { gap: 12 },
- primaryBtn: {
- backgroundColor: "#000",
- paddingVertical: 16,
- borderRadius: 12,
- alignItems: "center",
- },
- primaryBtnText: { color: "#fff", fontSize: 18, fontWeight: "700" },
- secondaryBtn: {
- paddingVertical: 14,
- alignItems: "center",
- },
- secondaryBtnText: { color: "#888", fontSize: 16, fontWeight: "500" },
- errorCard: {
- backgroundColor: "#FFF3F3",
- borderRadius: 16,
- padding: 32,
- alignItems: "center",
- marginBottom: 24,
- },
- errorIcon: {
- fontSize: 36,
- fontWeight: "bold",
- color: "#FF3B30",
- backgroundColor: "#FFE5E5",
- width: 56,
- height: 56,
- lineHeight: 56,
- borderRadius: 28,
- textAlign: "center",
- marginBottom: 16,
- overflow: "hidden",
- },
- errorTitle: {
- fontSize: 22,
- fontWeight: "700",
- color: "#222",
- marginBottom: 8,
- },
- errorBody: {
- fontSize: 15,
- color: "#888",
- textAlign: "center",
- lineHeight: 22,
- },
-});
+function makeStyles(colors: ReturnType["colors"]) {
+ return StyleSheet.create({
+ container: { flex: 1, backgroundColor: colors.background },
+ content: {
+ flex: 1,
+ padding: 24,
+ justifyContent: "center",
+ },
+ heading: {
+ fontSize: 32,
+ fontWeight: "bold",
+ color: colors.text,
+ marginBottom: 4,
+ },
+ subheading: {
+ fontSize: 16,
+ color: colors.textSecondary,
+ marginBottom: 32,
+ },
+ card: {
+ backgroundColor: colors.card,
+ borderRadius: 16,
+ padding: 20,
+ marginBottom: 40,
+ },
+ row: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ paddingVertical: 14,
+ },
+ rowLabel: { fontSize: 15, color: colors.textSecondary },
+ rowValue: {
+ fontSize: 16,
+ fontWeight: "500",
+ color: colors.text,
+ flexShrink: 1,
+ textAlign: "right",
+ },
+ rowValueHighlight: { fontSize: 20, fontWeight: "700", color: colors.text },
+ divider: { height: 1, backgroundColor: colors.border },
+ actions: { gap: 12 },
+ primaryBtn: {
+ backgroundColor: colors.primaryBtn,
+ paddingVertical: 16,
+ borderRadius: 12,
+ alignItems: "center",
+ },
+ primaryBtnText: { color: colors.primaryBtnText, fontSize: 18, fontWeight: "700" },
+ secondaryBtn: {
+ paddingVertical: 14,
+ alignItems: "center",
+ },
+ secondaryBtnText: { color: colors.textSecondary, fontSize: 16, fontWeight: "500" },
+ errorCard: {
+ backgroundColor: colors.errorSurface,
+ borderRadius: 16,
+ padding: 32,
+ alignItems: "center",
+ marginBottom: 24,
+ },
+ errorIcon: {
+ fontSize: 36,
+ fontWeight: "bold",
+ color: colors.error,
+ backgroundColor: colors.errorSurface,
+ width: 56,
+ height: 56,
+ lineHeight: 56,
+ borderRadius: 28,
+ textAlign: "center",
+ marginBottom: 16,
+ overflow: "hidden",
+ },
+ errorTitle: {
+ fontSize: 22,
+ fontWeight: "700",
+ color: colors.text,
+ marginBottom: 8,
+ },
+ errorBody: {
+ fontSize: 15,
+ color: colors.textSecondary,
+ textAlign: "center",
+ lineHeight: 22,
+ },
+ });
+}
diff --git a/app/mobile/app/quick-receive.tsx b/app/mobile/app/quick-receive.tsx
index a1cba0d..b0ae41d 100644
--- a/app/mobile/app/quick-receive.tsx
+++ b/app/mobile/app/quick-receive.tsx
@@ -5,11 +5,11 @@ import {
StyleSheet,
TouchableOpacity,
Share,
- useColorScheme,
Alert,
} from "react-native";
import QRCode from "react-native-qrcode-svg";
import * as Clipboard from "expo-clipboard";
+import { useAppTheme } from "@/context/ThemeContext";
// TODO: Replace this with real auth hook
const useUser = () => {
@@ -20,8 +20,8 @@ const useUser = () => {
export default function QuickReceiveScreen() {
const { username } = useUser();
- const colorScheme = useColorScheme();
- const isDark = colorScheme === "dark";
+ const { colors, colorScheme } = useAppTheme();
+ const s = makeStyles(colors);
const receiveLink = useMemo(() => {
if (!username) return null;
@@ -43,27 +43,24 @@ export default function QuickReceiveScreen() {
};
return (
-
-
- Quick Receive
-
+
+ Quick Receive
{!username ? (
-
-
- No username found.
-
-
- Claim one to start receiving payments.
-
+
+ No username found.
+ Claim one to start receiving payments.
) : (
<>
-
- @{username}
-
+ @{username}
-
+ {/*
+ * QR code always uses black-on-white for maximum readability.
+ * The white container ensures contrast in dark mode.
+ * This is the correct approach per issue #149.
+ */}
+
-
- Copy Link
+
+ {colorScheme === "dark"
+ ? "QR code shown in high-contrast mode for readability"
+ : "Scan this QR code to send a payment"}
+
+
+
+ Copy Link
-
- Share
+
+ Share
>
)}
@@ -91,65 +88,76 @@ export default function QuickReceiveScreen() {
);
}
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- padding: 24,
- alignItems: "center",
- justifyContent: "center",
- backgroundColor: "#ffffff",
- },
- darkContainer: {
- backgroundColor: "#121212",
- },
- title: {
- fontSize: 22,
- fontWeight: "600",
- marginBottom: 24,
- },
- username: {
- fontSize: 20,
- fontWeight: "bold",
- marginBottom: 20,
- },
- darkText: {
- color: "#ffffff",
- },
- qrWrapper: {
- padding: 16,
- backgroundColor: "#ffffff",
- borderRadius: 16,
- marginBottom: 30,
- },
- primaryButton: {
- width: "100%",
- backgroundColor: "#2563EB",
- padding: 14,
- borderRadius: 12,
- alignItems: "center",
- marginBottom: 12,
- },
- secondaryButton: {
- width: "100%",
- backgroundColor: "#10B981",
- padding: 14,
- borderRadius: 12,
- alignItems: "center",
- },
- buttonText: {
- color: "#ffffff",
- fontWeight: "600",
- },
- emptyContainer: {
- alignItems: "center",
- },
- warning: {
- fontSize: 16,
- fontWeight: "600",
- marginBottom: 6,
- },
- subText: {
- fontSize: 14,
- opacity: 0.7,
- },
-});
\ No newline at end of file
+function makeStyles(colors: ReturnType["colors"]) {
+ return StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 24,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.background,
+ },
+ title: {
+ fontSize: 22,
+ fontWeight: "600",
+ marginBottom: 24,
+ color: colors.text,
+ },
+ username: {
+ fontSize: 20,
+ fontWeight: "bold",
+ marginBottom: 20,
+ color: colors.text,
+ },
+ /*
+ * The QR wrapper uses a fixed white background so the QR code is always
+ * readable regardless of the app theme. This is intentional per the issue:
+ * "Ensure all charts and QR codes remain readable in dark mode."
+ */
+ qrWrapper: {
+ padding: 16,
+ backgroundColor: "#ffffff",
+ borderRadius: 16,
+ marginBottom: 12,
+ },
+ qrHint: {
+ fontSize: 12,
+ color: colors.textMuted,
+ marginBottom: 24,
+ textAlign: "center",
+ maxWidth: 260,
+ },
+ primaryButton: {
+ width: "100%",
+ backgroundColor: colors.tint,
+ padding: 14,
+ borderRadius: 12,
+ alignItems: "center",
+ marginBottom: 12,
+ },
+ secondaryButton: {
+ width: "100%",
+ backgroundColor: colors.success,
+ padding: 14,
+ borderRadius: 12,
+ alignItems: "center",
+ },
+ buttonText: {
+ color: "#ffffff",
+ fontWeight: "600",
+ },
+ emptyContainer: {
+ alignItems: "center",
+ },
+ warning: {
+ fontSize: 16,
+ fontWeight: "600",
+ marginBottom: 6,
+ color: colors.text,
+ },
+ subText: {
+ fontSize: 14,
+ color: colors.textSecondary,
+ },
+ });
+}
\ No newline at end of file
diff --git a/app/mobile/app/security.tsx b/app/mobile/app/security.tsx
index cd4415d..20d5181 100644
--- a/app/mobile/app/security.tsx
+++ b/app/mobile/app/security.tsx
@@ -11,6 +11,7 @@ import {
import { SafeAreaView } from "react-native-safe-area-context";
import { useSecurity } from "@/hooks/use-security";
+import { useAppTheme } from "@/context/ThemeContext";
export default function SecurityScreen() {
const {
@@ -21,6 +22,9 @@ export default function SecurityScreen() {
savePin,
} = useSecurity();
+ const { colors } = useAppTheme();
+ const s = makeStyles(colors);
+
const [pin, setPin] = useState("");
const [confirmPin, setConfirmPin] = useState("");
const [savingPin, setSavingPin] = useState(false);
@@ -67,18 +71,18 @@ export default function SecurityScreen() {
};
return (
-
-
- Security
-
+
+
+ Security
+
Protect sensitive flows with biometrics and a fallback PIN.
-
-
-
- Enable Biometric Lock
-
+
+
+
+ Enable Biometric Lock
+
Prompt on app open and before critical transactions.
@@ -89,28 +93,28 @@ export default function SecurityScreen() {
/>
-
+
-
+
{isBiometricAvailable
? "Biometric hardware is available on this device."
: "Biometrics unavailable. You can still set fallback PIN now and enable biometrics when available."}
-
-
+
+
{hasPinConfigured ? "Change Fallback PIN" : "Set Fallback PIN"}
-
+
PIN is stored as a hash in secure storage and used when biometrics
fail or are unavailable.
setPin(value.replace(/[^0-9]/g, ""))}
secureTextEntry
@@ -118,9 +122,9 @@ export default function SecurityScreen() {
maxLength={6}
/>
setConfirmPin(value.replace(/[^0-9]/g, ""))
@@ -131,11 +135,11 @@ export default function SecurityScreen() {
/>
-
+
{savingPin ? "Saving..." : "Save PIN"}
@@ -145,84 +149,87 @@ export default function SecurityScreen() {
);
}
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: "#FFFFFF",
- },
- content: {
- flex: 1,
- padding: 24,
- },
- title: {
- fontSize: 34,
- fontWeight: "800",
- color: "#111827",
- },
- subtitle: {
- marginTop: 8,
- fontSize: 16,
- color: "#6B7280",
- marginBottom: 26,
- lineHeight: 22,
- },
- card: {
- backgroundColor: "#F9FAFB",
- borderRadius: 16,
- padding: 16,
- marginBottom: 16,
- borderWidth: 1,
- borderColor: "#E5E7EB",
- },
- row: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- gap: 16,
- },
- rowTextWrap: {
- flex: 1,
- },
- rowTitle: {
- fontSize: 17,
- fontWeight: "700",
- color: "#111827",
- marginBottom: 4,
- },
- rowBody: {
- color: "#6B7280",
- fontSize: 14,
- lineHeight: 20,
- },
- supportText: {
- color: "#4B5563",
- fontSize: 13,
- },
- divider: {
- height: 1,
- backgroundColor: "#E5E7EB",
- marginVertical: 14,
- },
- input: {
- backgroundColor: "#fff",
- borderRadius: 10,
- borderWidth: 1,
- borderColor: "#D1D5DB",
- paddingHorizontal: 14,
- paddingVertical: 12,
- marginTop: 12,
- fontSize: 15,
- },
- saveBtn: {
- marginTop: 14,
- backgroundColor: "#111827",
- borderRadius: 12,
- alignItems: "center",
- paddingVertical: 14,
- },
- saveBtnText: {
- color: "#fff",
- fontWeight: "700",
- fontSize: 16,
- },
-});
+function makeStyles(colors: ReturnType["colors"]) {
+ return StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ content: {
+ flex: 1,
+ padding: 24,
+ },
+ title: {
+ fontSize: 34,
+ fontWeight: "800",
+ color: colors.text,
+ },
+ subtitle: {
+ marginTop: 8,
+ fontSize: 16,
+ color: colors.textSecondary,
+ marginBottom: 26,
+ lineHeight: 22,
+ },
+ card: {
+ backgroundColor: colors.surface,
+ borderRadius: 16,
+ padding: 16,
+ marginBottom: 16,
+ borderWidth: 1,
+ borderColor: colors.border,
+ },
+ row: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ gap: 16,
+ },
+ rowTextWrap: {
+ flex: 1,
+ },
+ rowTitle: {
+ fontSize: 17,
+ fontWeight: "700",
+ color: colors.text,
+ marginBottom: 4,
+ },
+ rowBody: {
+ color: colors.textSecondary,
+ fontSize: 14,
+ lineHeight: 20,
+ },
+ supportText: {
+ color: colors.textMuted,
+ fontSize: 13,
+ },
+ divider: {
+ height: 1,
+ backgroundColor: colors.border,
+ marginVertical: 14,
+ },
+ input: {
+ backgroundColor: colors.input,
+ borderRadius: 10,
+ borderWidth: 1,
+ borderColor: colors.inputBorder,
+ paddingHorizontal: 14,
+ paddingVertical: 12,
+ marginTop: 12,
+ fontSize: 15,
+ color: colors.text,
+ },
+ saveBtn: {
+ marginTop: 14,
+ backgroundColor: colors.primaryBtn,
+ borderRadius: 12,
+ alignItems: "center",
+ paddingVertical: 14,
+ },
+ saveBtnText: {
+ color: colors.primaryBtnText,
+ fontWeight: "700",
+ fontSize: 16,
+ },
+ });
+}
diff --git a/app/mobile/app/settings.tsx b/app/mobile/app/settings.tsx
index c19e596..7440315 100644
--- a/app/mobile/app/settings.tsx
+++ b/app/mobile/app/settings.tsx
@@ -1,36 +1,129 @@
import React from "react";
import { SafeAreaView } from "react-native-safe-area-context";
-import { View, Text, StyleSheet, Switch } from "react-native";
+import { View, Text, StyleSheet, Switch, TouchableOpacity } from "react-native";
import { useNotifications } from "../components/notifications/NotificationContext";
+import { useAppTheme, ThemePreference } from "@/context/ThemeContext";
+
+type ThemeOption = { label: string; value: ThemePreference; emoji: string };
+
+const THEME_OPTIONS: ThemeOption[] = [
+ { label: "Light", value: "light", emoji: "☀️" },
+ { label: "Dark", value: "dark", emoji: "🌙" },
+ { label: "System", value: "system", emoji: "📱" },
+];
export default function SettingsScreen() {
const { soundEnabled, setSoundEnabled } = useNotifications();
+ const { themePreference, setThemePreference, colors } = useAppTheme();
+
+ const s = makeStyles(colors);
return (
-
-
- Settings
+
+
+ Settings
-
- 🔔 Sound Effects
-
+ {/* ── Theme selector ── */}
+ 🎨 App Theme
+
+ {THEME_OPTIONS.map((opt, i) => {
+ const isSelected = themePreference === opt.value;
+ return (
+
+ {i > 0 && }
+ setThemePreference(opt.value)}
+ activeOpacity={0.7}
+ accessibilityRole="radio"
+ accessibilityState={{ checked: isSelected }}
+ >
+
+ {opt.emoji}{" "}{opt.label}
+
+
+ {isSelected && }
+
+
+
+ );
+ })}
+
+
+ {/* ── Notifications ── */}
+ 🔔 Notifications
+
+
+ Sound Effects
+
+
);
}
-const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: "#fff" },
- content: { padding: 24 },
- title: { fontSize: 28, fontWeight: "700", marginBottom: 16 },
- row: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- paddingVertical: 12,
- borderBottomWidth: 1,
- borderColor: "#f3f4f6",
- },
- label: { fontSize: 16 },
-});
+function makeStyles(colors: ReturnType["colors"]) {
+ return StyleSheet.create({
+ container: { flex: 1, backgroundColor: colors.background },
+ content: { padding: 24 },
+ title: {
+ fontSize: 28,
+ fontWeight: "700",
+ color: colors.text,
+ marginBottom: 24,
+ },
+ sectionHeader: {
+ fontSize: 13,
+ fontWeight: "600",
+ color: colors.textSecondary,
+ textTransform: "uppercase",
+ letterSpacing: 0.8,
+ marginBottom: 8,
+ marginTop: 8,
+ },
+ card: {
+ backgroundColor: colors.surface,
+ borderRadius: 12,
+ borderWidth: 1,
+ borderColor: colors.border,
+ marginBottom: 20,
+ overflow: "hidden",
+ },
+ themeRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ paddingHorizontal: 16,
+ paddingVertical: 14,
+ },
+ themeLabel: { fontSize: 16, color: colors.text },
+ radioOuter: {
+ width: 22,
+ height: 22,
+ borderRadius: 11,
+ borderWidth: 2,
+ borderColor: colors.border,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ radioOuterSelected: { borderColor: colors.tint },
+ radioInner: {
+ width: 10,
+ height: 10,
+ borderRadius: 5,
+ backgroundColor: colors.tint,
+ },
+ separator: { height: 1, backgroundColor: colors.border, marginHorizontal: 16 },
+ row: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ paddingHorizontal: 16,
+ paddingVertical: 14,
+ },
+ label: { fontSize: 16, color: colors.text },
+ });
+}
diff --git a/app/mobile/app/transactions.tsx b/app/mobile/app/transactions.tsx
index ccceacc..3e777cb 100644
--- a/app/mobile/app/transactions.tsx
+++ b/app/mobile/app/transactions.tsx
@@ -17,6 +17,7 @@ import { useTransactions } from '../hooks/use-transactions';
import type { TransactionItem as TransactionItemType } from '../types/transaction';
import { ErrorState } from '../components/resilience/error-state';
import { EmptyState } from '../components/resilience/empty-state';
+import { useAppTheme } from '@/context/ThemeContext';
/**
* Placeholder account used when no accountId is passed via route params.
@@ -26,15 +27,15 @@ const DEMO_ACCOUNT_ID =
// ─── Loading Skeleton ────────────────────────────────────────────────────────
-function SkeletonRow() {
+function SkeletonRow({ skeletonColor }: { skeletonColor: string }) {
return (
-
-
+
+
-
-
+
+
-
+
);
}
@@ -46,20 +47,18 @@ const skeleton = StyleSheet.create({
paddingHorizontal: 20,
paddingVertical: 14,
borderBottomWidth: StyleSheet.hairlineWidth,
- borderBottomColor: '#E5E7EB',
+ borderBottomColor: 'transparent',
},
circle: {
width: 44,
height: 44,
borderRadius: 22,
- backgroundColor: '#E5E7EB',
marginRight: 14,
},
lines: { flex: 1 },
line: {
height: 12,
borderRadius: 6,
- backgroundColor: '#E5E7EB',
},
});
@@ -67,6 +66,9 @@ const skeleton = StyleSheet.create({
export default function TransactionsScreen() {
const router = useRouter();
+ const { colors } = useAppTheme();
+ const s = makeStyles(colors);
+
const params = useLocalSearchParams<{ accountId?: string }>();
const accountId = (params.accountId ?? DEMO_ACCOUNT_ID).trim();
@@ -80,15 +82,15 @@ export default function TransactionsScreen() {
);
const ListHeader = (
-
- {shortAccount}
+
+ {shortAccount}
);
const ListEmpty = loading ? (
{[...Array(6)].map((_, i) => (
-
+
))}
) : error ? (
@@ -105,24 +107,24 @@ export default function TransactionsScreen() {
);
const ListFooter = hasMore ? (
-
-
+
+
) : null;
return (
-
+
{/* ── Header ── */}
-
+
router.back()}
- style={styles.backBtn}
+ style={s.backBtn}
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
>
- ‹
+ ‹
- Transaction History
-
+ Transaction History
+
{/* ── Transaction List ── */}
@@ -137,14 +139,14 @@ export default function TransactionsScreen() {
}
onEndReached={loadMore}
onEndReachedThreshold={0.8}
contentContainerStyle={
(transactions.length === 0 || error) && !loading
- ? styles.emptyFill
+ ? s.emptyFill
: undefined
}
showsVerticalScrollIndicator={false}
@@ -155,118 +157,67 @@ export default function TransactionsScreen() {
// ─── Styles ──────────────────────────────────────────────────────────────────
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#F9FAFB',
- },
-
- // Header
- header: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- paddingHorizontal: 16,
- paddingVertical: 12,
- backgroundColor: '#fff',
- borderBottomWidth: StyleSheet.hairlineWidth,
- borderBottomColor: '#E5E7EB',
- },
- headerTitle: {
- fontSize: 17,
- fontWeight: '600',
- color: '#111827',
- },
- backBtn: {
- width: 36,
- alignItems: 'center',
- },
- backChevron: {
- fontSize: 28,
- color: '#111827',
- lineHeight: 32,
- },
-
- // Error banner
- errorBanner: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: '#FEF2F2',
- borderBottomWidth: 1,
- borderBottomColor: '#FECACA',
- paddingHorizontal: 16,
- paddingVertical: 10,
- gap: 12,
- },
- errorText: {
- flex: 1,
- fontSize: 13,
- color: '#991B1B',
- lineHeight: 18,
- },
- retryBtn: {
- backgroundColor: '#DC2626',
- borderRadius: 6,
- paddingHorizontal: 14,
- paddingVertical: 6,
- },
- retryText: {
- color: '#fff',
- fontSize: 13,
- fontWeight: '600',
- },
-
- // List header
- listHeader: {
- paddingHorizontal: 20,
- paddingTop: 16,
- paddingBottom: 8,
- },
- accountPill: {
- alignSelf: 'flex-start',
- backgroundColor: '#E5E7EB',
- color: '#374151',
- fontSize: 12,
- fontWeight: '600',
- paddingHorizontal: 10,
- paddingVertical: 4,
- borderRadius: 99,
- overflow: 'hidden',
- fontFamily: 'monospace',
- },
-
- // Empty state
- emptyFill: {
- flexGrow: 1,
- },
- emptyContainer: {
- flex: 1,
- alignItems: 'center',
- justifyContent: 'center',
- paddingHorizontal: 40,
- paddingTop: 80,
- },
- emptyIcon: {
- fontSize: 48,
- marginBottom: 16,
- },
- emptyTitle: {
- fontSize: 18,
- fontWeight: '700',
- color: '#111827',
- marginBottom: 8,
- textAlign: 'center',
- },
- emptySubtitle: {
- fontSize: 14,
- color: '#6B7280',
- textAlign: 'center',
- lineHeight: 20,
- },
-
- // Footer (load-more indicator)
- footer: {
- paddingVertical: 20,
- alignItems: 'center',
- },
-});
+function makeStyles(colors: ReturnType["colors"]) {
+ return StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+
+ // Header
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ backgroundColor: colors.surface,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: colors.border,
+ },
+ headerTitle: {
+ fontSize: 17,
+ fontWeight: '600',
+ color: colors.text,
+ },
+ backBtn: {
+ width: 36,
+ alignItems: 'center',
+ },
+ backChevron: {
+ fontSize: 28,
+ color: colors.text,
+ lineHeight: 32,
+ },
+
+ // List header
+ listHeader: {
+ paddingHorizontal: 20,
+ paddingTop: 16,
+ paddingBottom: 8,
+ },
+ accountPill: {
+ alignSelf: 'flex-start',
+ backgroundColor: colors.pillBg,
+ color: colors.pillText,
+ fontSize: 12,
+ fontWeight: '600',
+ paddingHorizontal: 10,
+ paddingVertical: 4,
+ borderRadius: 99,
+ overflow: 'hidden',
+ fontFamily: 'monospace',
+ },
+
+ // Empty state
+ emptyFill: {
+ flexGrow: 1,
+ },
+
+ // Footer (load-more indicator)
+ footer: {
+ paddingVertical: 20,
+ alignItems: 'center',
+ },
+ });
+}
diff --git a/app/mobile/app/wallet-connect.tsx b/app/mobile/app/wallet-connect.tsx
index 67f5264..986eb8a 100644
--- a/app/mobile/app/wallet-connect.tsx
+++ b/app/mobile/app/wallet-connect.tsx
@@ -7,6 +7,7 @@ import { SafeAreaView } from "react-native-safe-area-context";
import { useNetworkStatus } from "../hooks/use-network-status";
import { useSecurity } from "../hooks/use-security";
import { usePaymentListener } from "../hooks/usePaymentListener";
+import { useAppTheme } from "@/context/ThemeContext";
type Network = "testnet" | "mainnet";
@@ -24,6 +25,8 @@ export default function WalletConnectScreen() {
getSensitiveSessionToken,
saveSensitiveSessionToken,
} = useSecurity();
+ const { colors } = useAppTheme();
+ const s = makeStyles(colors);
const [connected, setConnected] = useState(false);
const [network, setNetwork] = useState("testnet");
@@ -81,216 +84,218 @@ export default function WalletConnectScreen() {
};
return (
-
-
- Wallet Connection
-
+
+
+ Wallet Connection
+
Connect your Stellar wallet and protect sensitive wallet data with
biometric security.
-
-
- Network
+
+
+ Network
- {network.toUpperCase()}
+ {network.toUpperCase()}
-
- Status
-
+
+ Status
+
{connected ? "Connected" : "Not Connected"}
{!isConnected ? (
-
+
-
+
Internet connection is required to link a new wallet.
) : null}
{connected && publicKey ? (
- {publicKey}
+ {publicKey}
) : null}
{!connected ? (
-
- Connect Wallet
+
+ Connect Wallet
) : (
<>
-
+
Reveal Secure Session Token
{sessionTokenPreview ? (
-
+
Token: {sessionTokenPreview}
) : null}
- Disconnect
+ Disconnect
>
)}
- router.back()}>
- Go Back
+ router.back()}>
+ Go Back
);
}
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: "#fff",
- },
- content: {
- flex: 1,
- padding: 24,
- },
- title: {
- fontSize: 32,
- fontWeight: "bold",
- marginTop: 40,
- marginBottom: 12,
- color: "#111827",
- },
- subtitle: {
- fontSize: 16,
- color: "#6B7280",
- marginBottom: 28,
- lineHeight: 22,
- },
- card: {
- backgroundColor: "#F9FAFB",
- borderRadius: 16,
- borderWidth: 1,
- borderColor: "#E5E7EB",
- padding: 16,
- },
- row: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- marginBottom: 14,
- },
- label: {
- fontSize: 16,
- fontWeight: "700",
- color: "#111827",
- },
- networkBadge: {
- paddingHorizontal: 12,
- paddingVertical: 6,
- borderRadius: 8,
- },
- mainnet: {
- backgroundColor: "#10B981",
- },
- testnet: {
- backgroundColor: "#F59E0B",
- },
- networkText: {
- color: "#fff",
- fontWeight: "700",
- },
- connected: {
- color: "#10B981",
- fontWeight: "700",
- },
- disconnected: {
- color: "#EF4444",
- fontWeight: "700",
- },
- offlineAdvice: {
- flexDirection: "row",
- alignItems: "center",
- backgroundColor: "#FEF2F2",
- paddingVertical: 10,
- paddingHorizontal: 12,
- borderRadius: 8,
- marginBottom: 16,
- gap: 8,
- borderWidth: 1,
- borderColor: "#FECACA",
- },
- offlineAdviceText: {
- color: "#991B1B",
- fontSize: 13,
- fontWeight: "500",
- flex: 1,
- },
- address: {
- fontSize: 12,
- color: "#374151",
- marginBottom: 16,
- },
- connectButton: {
- backgroundColor: "#111827",
- padding: 16,
- borderRadius: 10,
- alignItems: "center",
- },
- disconnectButton: {
- backgroundColor: "#EF4444",
- padding: 16,
- borderRadius: 10,
- alignItems: "center",
- marginTop: 12,
- },
- secondaryButton: {
- borderColor: "#111827",
- borderWidth: 1,
- padding: 14,
- borderRadius: 10,
- alignItems: "center",
- },
- secondaryButtonText: {
- color: "#111827",
- fontWeight: "700",
- },
- tokenPreview: {
- marginTop: 10,
- fontSize: 13,
- color: "#4B5563",
- },
- buttonText: {
- color: "#fff",
- fontWeight: "700",
- fontSize: 16,
- },
- backButton: {
- marginTop: 22,
- alignItems: "center",
- },
- backButtonText: {
- color: "#6B7280",
- fontSize: 16,
- },
-});
+function makeStyles(colors: ReturnType["colors"]) {
+ return StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ content: {
+ flex: 1,
+ padding: 24,
+ },
+ title: {
+ fontSize: 32,
+ fontWeight: "bold",
+ marginTop: 40,
+ marginBottom: 12,
+ color: colors.text,
+ },
+ subtitle: {
+ fontSize: 16,
+ color: colors.textSecondary,
+ marginBottom: 28,
+ lineHeight: 22,
+ },
+ card: {
+ backgroundColor: colors.surface,
+ borderRadius: 16,
+ borderWidth: 1,
+ borderColor: colors.border,
+ padding: 16,
+ },
+ row: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginBottom: 14,
+ },
+ label: {
+ fontSize: 16,
+ fontWeight: "700",
+ color: colors.text,
+ },
+ networkBadge: {
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ borderRadius: 8,
+ },
+ mainnet: {
+ backgroundColor: colors.success,
+ },
+ testnet: {
+ backgroundColor: colors.warning,
+ },
+ networkText: {
+ color: "#fff",
+ fontWeight: "700",
+ },
+ connected: {
+ color: colors.success,
+ fontWeight: "700",
+ },
+ disconnected: {
+ color: colors.error,
+ fontWeight: "700",
+ },
+ offlineAdvice: {
+ flexDirection: "row",
+ alignItems: "center",
+ backgroundColor: colors.errorSurface,
+ paddingVertical: 10,
+ paddingHorizontal: 12,
+ borderRadius: 8,
+ marginBottom: 16,
+ gap: 8,
+ borderWidth: 1,
+ borderColor: colors.errorBorder,
+ },
+ offlineAdviceText: {
+ color: colors.errorText,
+ fontSize: 13,
+ fontWeight: "500",
+ flex: 1,
+ },
+ address: {
+ fontSize: 12,
+ color: colors.textSecondary,
+ marginBottom: 16,
+ },
+ connectButton: {
+ backgroundColor: colors.primaryBtn,
+ padding: 16,
+ borderRadius: 10,
+ alignItems: "center",
+ },
+ disconnectButton: {
+ backgroundColor: colors.error,
+ padding: 16,
+ borderRadius: 10,
+ alignItems: "center",
+ marginTop: 12,
+ },
+ secondaryButton: {
+ borderColor: colors.secondaryBtnBorder,
+ borderWidth: 1,
+ padding: 14,
+ borderRadius: 10,
+ alignItems: "center",
+ },
+ secondaryButtonText: {
+ color: colors.secondaryBtnText,
+ fontWeight: "700",
+ },
+ tokenPreview: {
+ marginTop: 10,
+ fontSize: 13,
+ color: colors.textSecondary,
+ },
+ buttonText: {
+ color: colors.primaryBtnText,
+ fontWeight: "700",
+ fontSize: 16,
+ },
+ backButton: {
+ marginTop: 22,
+ alignItems: "center",
+ },
+ backButtonText: {
+ color: colors.textSecondary,
+ fontSize: 16,
+ },
+ });
+}
diff --git a/app/mobile/components/transaction-item.tsx b/app/mobile/components/transaction-item.tsx
index 133ae72..4f9ca32 100644
--- a/app/mobile/components/transaction-item.tsx
+++ b/app/mobile/components/transaction-item.tsx
@@ -7,6 +7,7 @@ import {
Clipboard,
} from 'react-native';
import type { TransactionItem as TransactionItemType } from '../types/transaction';
+import { useAppTheme } from '@/context/ThemeContext';
interface Props {
item: TransactionItemType;
@@ -36,6 +37,8 @@ function shortenHash(hash: string): string {
}
export default function TransactionItem({ item }: Props) {
+ const { colors } = useAppTheme();
+ const s = makeStyles(colors);
const assetLabel = formatAsset(item.asset);
const handleCopyHash = () => {
@@ -43,100 +46,102 @@ export default function TransactionItem({ item }: Props) {
};
return (
-
+
{/* Left: icon + asset */}
-
- {assetLabel.slice(0, 3)}
+
+ {assetLabel.slice(0, 3)}
{/* Middle: asset name, memo, date */}
-
-
+
+
{assetLabel}
{item.memo ? (
-
+
{item.memo}
) : null}
- {shortenHash(item.txHash)}
+ {shortenHash(item.txHash)}
- {formatDate(item.timestamp)}
+ {formatDate(item.timestamp)}
{/* Right: amount */}
-
-
+
+
{parseFloat(item.amount).toFixed(2)}
- {assetLabel}
+ {assetLabel}
);
}
-const styles = StyleSheet.create({
- row: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingHorizontal: 20,
- paddingVertical: 14,
- borderBottomWidth: StyleSheet.hairlineWidth,
- borderBottomColor: '#E5E7EB',
- backgroundColor: '#fff',
- },
- iconWrap: {
- width: 44,
- height: 44,
- borderRadius: 22,
- backgroundColor: '#F3F4F6',
- alignItems: 'center',
- justifyContent: 'center',
- marginRight: 14,
- },
- assetIcon: {
- fontSize: 13,
- fontWeight: '700',
- color: '#374151',
- letterSpacing: -0.5,
- },
- middle: {
- flex: 1,
- gap: 2,
- },
- assetName: {
- fontSize: 15,
- fontWeight: '600',
- color: '#111827',
- },
- memo: {
- fontSize: 13,
- color: '#6B7280',
- },
- txHash: {
- fontSize: 11,
- color: '#9CA3AF',
- fontFamily: 'monospace',
- },
- date: {
- fontSize: 12,
- color: '#9CA3AF',
- marginTop: 1,
- },
- right: {
- alignItems: 'flex-end',
- marginLeft: 8,
- maxWidth: 110,
- },
- amount: {
- fontSize: 15,
- fontWeight: '700',
- color: '#111827',
- },
- assetCode: {
- fontSize: 12,
- color: '#6B7280',
- marginTop: 2,
- },
-});
+function makeStyles(colors: ReturnType["colors"]) {
+ return StyleSheet.create({
+ row: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ paddingVertical: 14,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: colors.border,
+ backgroundColor: colors.background,
+ },
+ iconWrap: {
+ width: 44,
+ height: 44,
+ borderRadius: 22,
+ backgroundColor: colors.surfaceAlt,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginRight: 14,
+ },
+ assetIcon: {
+ fontSize: 13,
+ fontWeight: '700',
+ color: colors.textSecondary,
+ letterSpacing: -0.5,
+ },
+ middle: {
+ flex: 1,
+ gap: 2,
+ },
+ assetName: {
+ fontSize: 15,
+ fontWeight: '600',
+ color: colors.text,
+ },
+ memo: {
+ fontSize: 13,
+ color: colors.textSecondary,
+ },
+ txHash: {
+ fontSize: 11,
+ color: colors.textMuted,
+ fontFamily: 'monospace',
+ },
+ date: {
+ fontSize: 12,
+ color: colors.textMuted,
+ marginTop: 1,
+ },
+ right: {
+ alignItems: 'flex-end',
+ marginLeft: 8,
+ maxWidth: 110,
+ },
+ amount: {
+ fontSize: 15,
+ fontWeight: '700',
+ color: colors.text,
+ },
+ assetCode: {
+ fontSize: 12,
+ color: colors.textSecondary,
+ marginTop: 2,
+ },
+ });
+}
diff --git a/app/mobile/constants/theme.ts b/app/mobile/constants/theme.ts
index f06facd..78aaa81 100644
--- a/app/mobile/constants/theme.ts
+++ b/app/mobile/constants/theme.ts
@@ -1,6 +1,7 @@
/**
- * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
- * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
+ * Theme tokens for the QuickEx mobile app.
+ * All colours are defined here in light and dark mode.
+ * Screens should consume these via `useAppTheme()` – never use hardcoded values.
*/
import { Platform } from 'react-native';
@@ -10,20 +11,94 @@ const tintColorDark = '#fff';
export const Colors = {
light: {
+ // Base
text: '#11181C',
- background: '#fff',
+ textSecondary: '#6B7280',
+ textMuted: '#9CA3AF',
+ background: '#ffffff',
+ surface: '#F9FAFB',
+ surfaceAlt: '#F3F4F6',
+ border: '#E5E7EB',
+ borderLight: '#F3F4F6',
+
+ // Brand / tint
tint: tintColorLight,
icon: '#687076',
tabIconDefault: '#687076',
tabIconSelected: tintColorLight,
+
+ // Buttons
+ primaryBtn: '#111827',
+ primaryBtnText: '#ffffff',
+ secondaryBtnBorder: '#111827',
+ secondaryBtnText: '#111827',
+
+ // Cards / inputs
+ card: '#F5F5F5',
+ cardBorder: '#E5E7EB',
+ input: '#ffffff',
+ inputBorder: '#D1D5DB',
+ inputPlaceholder: '#9CA3AF',
+
+ // Status colours (unchanged between themes – semantic)
+ success: '#10B981',
+ warning: '#F59E0B',
+ error: '#EF4444',
+ errorSurface: '#FEF2F2',
+ errorBorder: '#FECACA',
+ errorText: '#991B1B',
+
+ // Account pill
+ pillBg: '#E5E7EB',
+ pillText: '#374151',
+
+ // Skeleton / shimmer
+ skeleton: '#E5E7EB',
},
dark: {
+ // Base
text: '#ECEDEE',
- background: '#151718',
+ textSecondary: '#9CA3AF',
+ textMuted: '#6B7280',
+ background: '#0F1117',
+ surface: '#1A1D27',
+ surfaceAlt: '#23273A',
+ border: '#2D3147',
+ borderLight: '#1E2235',
+
+ // Brand / tint
tint: tintColorDark,
icon: '#9BA1A6',
tabIconDefault: '#9BA1A6',
tabIconSelected: tintColorDark,
+
+ // Buttons
+ primaryBtn: '#ECEDEE',
+ primaryBtnText: '#0F1117',
+ secondaryBtnBorder: '#4B5563',
+ secondaryBtnText: '#ECEDEE',
+
+ // Cards / inputs
+ card: '#1A1D27',
+ cardBorder: '#2D3147',
+ input: '#23273A',
+ inputBorder: '#374151',
+ inputPlaceholder: '#6B7280',
+
+ // Status colours
+ success: '#10B981',
+ warning: '#F59E0B',
+ error: '#EF4444',
+ errorSurface: '#2D1515',
+ errorBorder: '#7F1D1D',
+ errorText: '#FCA5A5',
+
+ // Account pill
+ pillBg: '#23273A',
+ pillText: '#9CA3AF',
+
+ // Skeleton / shimmer
+ skeleton: '#23273A',
},
};
diff --git a/app/mobile/context/ThemeContext.tsx b/app/mobile/context/ThemeContext.tsx
new file mode 100644
index 0000000..8fc282e
--- /dev/null
+++ b/app/mobile/context/ThemeContext.tsx
@@ -0,0 +1,95 @@
+/**
+ * ThemeContext
+ *
+ * Wraps the whole app and provides:
+ * - `themePreference` – what the user explicitly chose ('light' | 'dark' | 'system')
+ * - `colorScheme` – the resolved scheme that is currently active ('light' | 'dark')
+ * - `colors` – the Colors token set for the resolved scheme
+ * - `setThemePreference` – persist and apply a new preference
+ *
+ * Preference is stored in AsyncStorage so it survives app restarts without
+ * requiring a reload.
+ */
+
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import React, {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useState,
+} from 'react';
+import { useColorScheme } from 'react-native';
+
+import { Colors } from '@/constants/theme';
+
+export type ThemePreference = 'light' | 'dark' | 'system';
+export type ColorScheme = 'light' | 'dark';
+
+interface ThemeContextValue {
+ /** What the user explicitly selected */
+ themePreference: ThemePreference;
+ /** The resolved scheme that is currently active */
+ colorScheme: ColorScheme;
+ /** Color tokens for the active scheme – use these in StyleSheet */
+ colors: typeof Colors.light;
+ /** Persist and immediately apply a new preference */
+ setThemePreference: (preference: ThemePreference) => Promise;
+}
+
+const STORAGE_KEY = '@quickex/theme_preference';
+
+const ThemeContext = createContext({
+ themePreference: 'system',
+ colorScheme: 'light',
+ colors: Colors.light,
+ setThemePreference: async () => {},
+});
+
+export function ThemeProvider({ children }: { children: React.ReactNode }) {
+ const systemScheme = useColorScheme(); // 'light' | 'dark' | null
+ const [preference, setPreference] = useState('system');
+
+ // Load persisted preference on mount
+ useEffect(() => {
+ AsyncStorage.getItem(STORAGE_KEY)
+ .then((stored) => {
+ if (stored === 'light' || stored === 'dark' || stored === 'system') {
+ setPreference(stored);
+ }
+ })
+ .catch(() => {
+ // If storage fails, fall back to 'system' – already the default state
+ });
+ }, []);
+
+ const setThemePreference = useCallback(async (pref: ThemePreference) => {
+ setPreference(pref);
+ try {
+ await AsyncStorage.setItem(STORAGE_KEY, pref);
+ } catch {
+ // Silently ignore storage errors; the in-memory state change still applies
+ }
+ }, []);
+
+ // Resolve the effective scheme
+ const colorScheme: ColorScheme =
+ preference === 'system'
+ ? (systemScheme ?? 'light')
+ : preference;
+
+ const colors = Colors[colorScheme];
+
+ return (
+
+ {children}
+
+ );
+}
+
+/** Hook – use this in every screen/component instead of useColorScheme directly */
+export function useAppTheme(): ThemeContextValue {
+ return useContext(ThemeContext);
+}