@magicred-1/react-native-lxmf 0.2.34 → 0.2.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,258 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import {
3
+ PermissionsAndroid,
4
+ Platform,
5
+ Pressable,
6
+ ScrollView,
7
+ StyleSheet,
8
+ Switch,
9
+ Text,
10
+ TextInput,
11
+ View,
12
+ } from 'react-native';
13
+ import { LxmfModule, LxmfNodeMode } from '@magicred-1/react-native-lxmf';
14
+ import { useLxmfContext } from '@/context/LxmfContext';
15
+
16
+ function shortHex(v: string): string {
17
+ if (!v || v.length <= 12) return v || '—';
18
+ return `${v.slice(0, 6)}…${v.slice(-6)}`;
19
+ }
20
+
21
+ type TransportTab = 'ble' | 'tcp' | 'both';
22
+
23
+ // ── Stat row ──────────────────────────────────────────────────────────────────
24
+
25
+ function Row({ label, value }: Readonly<{ label: string; value: string }>) {
26
+ return (
27
+ <View style={S.statRow}>
28
+ <Text style={S.statLabel}>{label}</Text>
29
+ <Text selectable style={S.statValue}>{value}</Text>
30
+ </View>
31
+ );
32
+ }
33
+
34
+ // ── Main screen ───────────────────────────────────────────────────────────────
35
+
36
+ export default function NetworkScreen() {
37
+ const { isRunning, isNativeAvailable, status, error, identityHydrated, displayName, start, stop, getStatus } = useLxmfContext();
38
+
39
+ const [tab, setTab] = useState<TransportTab>('both');
40
+ const [tcpHost, setTcpHost] = useState('192.168.1.135');
41
+ const [tcpPort, setTcpPort] = useState('4243');
42
+ const [localName, setLocalName] = useState(displayName);
43
+ const [isBeacon, setIsBeacon] = useState(false);
44
+ const [msg, setMsg] = useState('');
45
+ const [bleCount, setBleCount] = useState(0);
46
+ const [unpairedCount, setUnpairedCount] = useState(0);
47
+
48
+ // Poll BLE peer count while running
49
+ useEffect(() => {
50
+ if (!isRunning) { setBleCount(0); setUnpairedCount(0); return; }
51
+ const tick = () => {
52
+ try { setBleCount(LxmfModule.blePeerCount()); } catch {}
53
+ try { setUnpairedCount(LxmfModule.bleUnpairedRNodeCount()); } catch {}
54
+ };
55
+ tick();
56
+ const id = setInterval(tick, 2000);
57
+ return () => clearInterval(id);
58
+ }, [isRunning]);
59
+
60
+ // Auto-refresh status
61
+ useEffect(() => {
62
+ if (!isRunning) return;
63
+ const id = setInterval(getStatus, 5000);
64
+ return () => clearInterval(id);
65
+ }, [isRunning, getStatus]);
66
+
67
+ const requestBlePerms = async (): Promise<boolean> => {
68
+ if (Platform.OS !== 'android') return true;
69
+ const perms = Platform.Version >= 31
70
+ ? [PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT]
71
+ : [PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION];
72
+ const res = await PermissionsAndroid.requestMultiple(perms);
73
+ if (Object.values(res).some(r => r !== PermissionsAndroid.RESULTS.GRANTED)) {
74
+ setMsg('BLE permissions denied.');
75
+ return false;
76
+ }
77
+ return true;
78
+ };
79
+
80
+ const onStart = useCallback(async () => {
81
+ setMsg('');
82
+ const name = localName.trim() || 'lxmf-mobile';
83
+
84
+ if (tab === 'ble') {
85
+ if (!await requestBlePerms()) return;
86
+ const ok = await start({ mode: LxmfNodeMode.BleOnly, displayName: name, isBeacon });
87
+ if (!ok) setMsg('Failed to start BLE node.');
88
+ } else {
89
+ const host = tcpHost.trim();
90
+ const port = Number(tcpPort);
91
+ if (!host) { setMsg('Host required.'); return; }
92
+ if (!Number.isInteger(port) || port < 1 || port > 65535) { setMsg('Port must be 1–65535.'); return; }
93
+ if (tab === 'both' && !await requestBlePerms()) return;
94
+ const mode = tab === 'tcp' ? LxmfNodeMode.Reticulum : LxmfNodeMode.ReticulumAndBle;
95
+ const ok = await start({ mode, tcpInterfaces: [{ host, port }], displayName: name, isBeacon });
96
+ if (!ok) setMsg('Failed to start node.');
97
+ }
98
+ }, [tab, tcpHost, tcpPort, localName, isBeacon, start]);
99
+
100
+ const onStop = useCallback(async () => {
101
+ await stop();
102
+ setMsg('');
103
+ }, [stop]);
104
+
105
+ const modeLabel = (m: number) => ['BLE', 'TCP', 'TCP Server', 'Reticulum', 'Reticulum+BLE'][m] ?? String(m);
106
+
107
+ const hasTcp = tab !== 'ble';
108
+
109
+ return (
110
+ <ScrollView style={S.root} contentContainerStyle={S.scroll}>
111
+ <View style={S.header}>
112
+ <Text style={S.headerTitle}>Network</Text>
113
+ </View>
114
+
115
+ {/* Error banner */}
116
+ {error ? <View style={S.errorBanner}><Text style={S.errorText}>{error}</Text></View> : null}
117
+
118
+ {/* ── Transport card ───────────────────────────────────────────── */}
119
+ <View style={S.card}>
120
+ <Text style={S.cardTitle}>Transport</Text>
121
+
122
+ {/* Segment control */}
123
+ <View style={S.segment}>
124
+ {(['ble', 'tcp', 'both'] as TransportTab[]).map(t => (
125
+ <Pressable
126
+ key={t}
127
+ style={[S.segBtn, tab === t && S.segBtnActive]}
128
+ onPress={() => { setTab(t); setMsg(''); }}
129
+ disabled={isRunning}>
130
+ <Text style={[S.segText, tab === t && S.segTextActive]}>
131
+ {t === 'ble' ? 'BLE' : t === 'tcp' ? 'TCP' : 'TCP+BLE'}
132
+ </Text>
133
+ </Pressable>
134
+ ))}
135
+ </View>
136
+
137
+ {/* TCP fields */}
138
+ {hasTcp && (
139
+ <>
140
+ <TextInput style={S.input} placeholder="Host" placeholderTextColor="#4a6070"
141
+ value={tcpHost} onChangeText={setTcpHost} autoCapitalize="none" autoCorrect={false} editable={!isRunning} />
142
+ <TextInput style={S.input} placeholder="Port" placeholderTextColor="#4a6070"
143
+ value={tcpPort} onChangeText={setTcpPort} keyboardType="number-pad" editable={!isRunning} />
144
+ </>
145
+ )}
146
+
147
+ {/* Display name */}
148
+ <TextInput style={S.input} placeholder="Display name" placeholderTextColor="#4a6070"
149
+ value={localName} onChangeText={setLocalName} autoCapitalize="none" autoCorrect={false} editable={!isRunning} />
150
+
151
+ {/* Beacon toggle */}
152
+ <View style={S.switchRow}>
153
+ <Text style={S.switchLabel}>Beacon mode</Text>
154
+ <Switch
155
+ value={isBeacon} onValueChange={setIsBeacon} disabled={isRunning}
156
+ trackColor={{ false: '#1e3040', true: '#1a7fc1' }}
157
+ thumbColor={isBeacon ? '#4fb3e8' : '#4a6070'}
158
+ />
159
+ </View>
160
+
161
+ {msg ? <Text style={S.warn}>{msg}</Text> : null}
162
+
163
+ {unpairedCount > 0 && (
164
+ <Text style={S.warn}>
165
+ {unpairedCount} unpaired RNode{unpairedCount > 1 ? 's' : ''} nearby — pair in Settings &gt; Bluetooth first.
166
+ </Text>
167
+ )}
168
+
169
+ <View style={S.btnRow}>
170
+ <Pressable
171
+ style={({ pressed }) => [S.btn, (!isNativeAvailable || isRunning || !identityHydrated) && S.btnDisabled, pressed && S.btnPressed]}
172
+ onPress={onStart}
173
+ disabled={!isNativeAvailable || isRunning || !identityHydrated}>
174
+ <Text style={S.btnText}>Start</Text>
175
+ </Pressable>
176
+ <Pressable
177
+ style={({ pressed }) => [S.btn, S.btnDanger, !isRunning && S.btnDisabled, pressed && S.btnPressed]}
178
+ onPress={onStop}
179
+ disabled={!isRunning}>
180
+ <Text style={S.btnText}>Stop</Text>
181
+ </Pressable>
182
+ </View>
183
+ </View>
184
+
185
+ {/* ── Node Status card ─────────────────────────────────────────── */}
186
+ <View style={S.card}>
187
+ <View style={S.cardTitleRow}>
188
+ <Text style={S.cardTitle}>Node Status</Text>
189
+ <Pressable style={({ pressed }) => [S.refreshBtn, pressed && { opacity: 0.7 }]} onPress={getStatus}>
190
+ <Text style={S.refreshText}>↻ Refresh</Text>
191
+ </Pressable>
192
+ </View>
193
+
194
+ <Row label="State" value={isRunning ? '● Running' : '○ Stopped'} />
195
+ <Row label="Mode" value={status ? modeLabel(status.mode) : '—'} />
196
+ <Row label="Address" value={status?.addressHex ? shortHex(status.addressHex) : '—'} />
197
+ <Row label="BLE peers" value={String(bleCount)} />
198
+ <Row label="Pending outbound" value={String(status?.pendingOutbound ?? 0)} />
199
+ <Row label="Messages sent" value={String(status?.outboundSent ?? 0)} />
200
+ <Row label="Messages received" value={String(status?.lxmfMessagesReceived ?? 0)} />
201
+ <Row label="Announces received" value={String(status?.announcesReceived ?? 0)} />
202
+ <Row label="Inbound accepted" value={String(status?.inboundAccepted ?? 0)} />
203
+ </View>
204
+ </ScrollView>
205
+ );
206
+ }
207
+
208
+ // ── Styles ────────────────────────────────────────────────────────────────────
209
+
210
+ const S = StyleSheet.create({
211
+ root: { flex: 1, backgroundColor: '#0c1218' },
212
+ scroll: { paddingBottom: 40, gap: 12 },
213
+
214
+ header: {
215
+ paddingHorizontal: 16, paddingTop: 56, paddingBottom: 14,
216
+ backgroundColor: '#131d26', borderBottomWidth: 1, borderBottomColor: '#1e3040',
217
+ },
218
+ headerTitle: { color: '#d8ecf8', fontSize: 28, fontWeight: '700' },
219
+
220
+ errorBanner: { backgroundColor: '#3a1515', borderWidth: 1, borderColor: '#7a2020', padding: 10, marginHorizontal: 14, borderRadius: 10 },
221
+ errorText: { color: '#ff9a9a', fontSize: 13 },
222
+
223
+ card: {
224
+ backgroundColor: '#131d26', borderRadius: 14, borderWidth: 1,
225
+ borderColor: '#1e3040', padding: 16, gap: 10, marginHorizontal: 14,
226
+ },
227
+ cardTitle: { color: '#d8ecf8', fontSize: 16, fontWeight: '700' },
228
+ cardTitleRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
229
+ refreshBtn: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8, backgroundColor: '#0d3550', borderWidth: 1, borderColor: '#1e3040' },
230
+ refreshText: { color: '#4fb3e8', fontSize: 12, fontWeight: '600' },
231
+
232
+ segment: { flexDirection: 'row', borderRadius: 10, borderWidth: 1, borderColor: '#1e3040', overflow: 'hidden' },
233
+ segBtn: { flex: 1, paddingVertical: 9, alignItems: 'center', backgroundColor: '#0e1923' },
234
+ segBtnActive: { backgroundColor: '#0d3550' },
235
+ segText: { color: '#4a6070', fontSize: 13, fontWeight: '600' },
236
+ segTextActive: { color: '#4fb3e8' },
237
+
238
+ input: {
239
+ borderWidth: 1, borderColor: '#2a4050', backgroundColor: '#0b1820', color: '#d8ecf8',
240
+ borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, fontFamily: 'monospace', fontSize: 13,
241
+ },
242
+
243
+ switchRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 2 },
244
+ switchLabel: { color: '#7a9db5', fontSize: 13 },
245
+
246
+ warn: { color: '#f0a500', fontSize: 12, fontFamily: 'monospace' },
247
+
248
+ btnRow: { flexDirection: 'row', gap: 8 },
249
+ btn: { flex: 1, borderRadius: 10, paddingVertical: 11, alignItems: 'center', backgroundColor: '#1a7fc1' },
250
+ btnDanger: { backgroundColor: '#c0392b' },
251
+ btnDisabled: { opacity: 0.4 },
252
+ btnPressed: { opacity: 0.75 },
253
+ btnText: { color: '#e8f6ff', fontSize: 14, fontWeight: '600' },
254
+
255
+ statRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 2 },
256
+ statLabel: { color: '#7a9db5', fontSize: 13 },
257
+ statValue: { color: '#d8ecf8', fontSize: 13, fontFamily: 'monospace' },
258
+ });
@@ -0,0 +1,230 @@
1
+ import { useCallback, useState } from 'react';
2
+ import {
3
+ Alert,
4
+ Modal,
5
+ Pressable,
6
+ ScrollView,
7
+ Share,
8
+ StyleSheet,
9
+ Text,
10
+ TextInput,
11
+ View,
12
+ } from 'react-native';
13
+ import { useLxmfContext } from '@/context/LxmfContext';
14
+
15
+ function shortHex(v: string): string {
16
+ if (!v || v.length <= 12) return v || '—';
17
+ return `${v.slice(0, 6)}…${v.slice(-6)}`;
18
+ }
19
+
20
+ function Row({ label, value, onAction, actionLabel }: Readonly<{
21
+ label: string; value: string; onAction?: () => void; actionLabel?: string;
22
+ }>) {
23
+ return (
24
+ <View style={S.row}>
25
+ <View style={S.rowLeft}>
26
+ <Text style={S.rowLabel}>{label}</Text>
27
+ <Text selectable style={S.rowValue}>{value}</Text>
28
+ </View>
29
+ {onAction && actionLabel && (
30
+ <Pressable style={({ pressed }) => [S.actionBtn, pressed && { opacity: 0.7 }]} onPress={onAction}>
31
+ <Text style={S.actionBtnText}>{actionLabel}</Text>
32
+ </Pressable>
33
+ )}
34
+ </View>
35
+ );
36
+ }
37
+
38
+ const LOG_LEVELS = ['0 – Off', '1 – Error', '2 – Warn', '3 – Info', '4 – Debug', '5 – Trace'];
39
+
40
+ export default function SettingsScreen() {
41
+ const {
42
+ identity, status, isRunning, displayName, setDisplayName, clearIdentity, setLogLevel,
43
+ } = useLxmfContext();
44
+
45
+ const [nameInput, setNameInput] = useState(displayName);
46
+ const [logLevel, setLogLevelState] = useState(3);
47
+ const [confirmReset, setConfirmReset] = useState(false);
48
+ const [nameSaved, setNameSaved] = useState(false);
49
+
50
+ const saveDisplayName = useCallback(() => {
51
+ setDisplayName(nameInput.trim() || 'lxmf-mobile');
52
+ setNameSaved(true);
53
+ setTimeout(() => setNameSaved(false), 2000);
54
+ }, [nameInput, setDisplayName]);
55
+
56
+ const changeLogLevel = useCallback((delta: number) => {
57
+ const next = Math.max(0, Math.min(5, logLevel + delta));
58
+ setLogLevelState(next);
59
+ setLogLevel(next);
60
+ }, [logLevel, setLogLevel]);
61
+
62
+ const copyAddress = useCallback(() => {
63
+ const addr = status?.addressHex ?? identity?.address_hex;
64
+ if (addr) Share.share({ message: addr }).catch(() => {});
65
+ }, [status?.addressHex, identity?.address_hex]);
66
+
67
+ const doReset = useCallback(async () => {
68
+ setConfirmReset(false);
69
+ await clearIdentity();
70
+ }, [clearIdentity]);
71
+
72
+ const displayAddr = status?.addressHex ?? identity?.address_hex ?? null;
73
+ const createdAt = identity?.created_at
74
+ ? new Date(identity.created_at).toLocaleDateString()
75
+ : '—';
76
+
77
+ return (
78
+ <ScrollView style={S.root} contentContainerStyle={S.scroll}>
79
+ <View style={S.header}>
80
+ <Text style={S.headerTitle}>Settings</Text>
81
+ </View>
82
+
83
+ {/* Display name */}
84
+ <View style={S.section}>
85
+ <Text style={S.sectionTitle}>Display Name</Text>
86
+ <Text style={S.hint}>Shown to peers in LXMF announces.</Text>
87
+ <View style={S.inputRow}>
88
+ <TextInput
89
+ style={[S.input, S.inputFlex]}
90
+ value={nameInput}
91
+ onChangeText={setNameInput}
92
+ placeholder="lxmf-mobile"
93
+ placeholderTextColor="#4a6070"
94
+ autoCapitalize="none"
95
+ autoCorrect={false}
96
+ />
97
+ <Pressable style={({ pressed }) => [S.saveBtn, pressed && { opacity: 0.75 }]} onPress={saveDisplayName}>
98
+ <Text style={S.saveBtnText}>{nameSaved ? '✓' : 'Save'}</Text>
99
+ </Pressable>
100
+ </View>
101
+ </View>
102
+
103
+ {/* Identity */}
104
+ <View style={S.section}>
105
+ <Text style={S.sectionTitle}>My Identity</Text>
106
+ {displayAddr ? (
107
+ <>
108
+ <Row
109
+ label="Address"
110
+ value={shortHex(displayAddr)}
111
+ onAction={copyAddress}
112
+ actionLabel="⎘ Copy"
113
+ />
114
+ <Row label="Full address" value={displayAddr} />
115
+ <Row label="Created" value={createdAt} />
116
+ </>
117
+ ) : (
118
+ <Text style={S.hint}>No identity yet — start the node in the Network tab.</Text>
119
+ )}
120
+ </View>
121
+
122
+ {/* Log level */}
123
+ <View style={S.section}>
124
+ <Text style={S.sectionTitle}>Log Level</Text>
125
+ <View style={S.logRow}>
126
+ <Pressable style={({ pressed }) => [S.stepBtn, pressed && { opacity: 0.7 }]} onPress={() => changeLogLevel(-1)}>
127
+ <Text style={S.stepBtnText}>−</Text>
128
+ </Pressable>
129
+ <Text style={S.logLabel}>{LOG_LEVELS[logLevel] ?? String(logLevel)}</Text>
130
+ <Pressable style={({ pressed }) => [S.stepBtn, pressed && { opacity: 0.7 }]} onPress={() => changeLogLevel(1)}>
131
+ <Text style={S.stepBtnText}>+</Text>
132
+ </Pressable>
133
+ </View>
134
+ </View>
135
+
136
+ {/* Danger zone */}
137
+ <View style={[S.section, S.dangerSection]}>
138
+ <Text style={[S.sectionTitle, S.dangerTitle]}>Danger Zone</Text>
139
+ <Text style={S.hint}>
140
+ Resetting your identity permanently removes your private key. You will lose access to your LXMF address and any pending messages.
141
+ </Text>
142
+ <Pressable
143
+ style={({ pressed }) => [S.dangerBtn, isRunning && S.btnDisabled, pressed && { opacity: 0.8 }]}
144
+ onPress={() => setConfirmReset(true)}
145
+ disabled={isRunning}>
146
+ <Text style={S.dangerBtnText}>{isRunning ? 'Stop node before resetting' : 'Reset Identity'}</Text>
147
+ </Pressable>
148
+ </View>
149
+
150
+ {/* Confirm reset modal */}
151
+ <Modal visible={confirmReset} transparent animationType="fade" onRequestClose={() => setConfirmReset(false)}>
152
+ <Pressable style={S.overlay} onPress={() => setConfirmReset(false)}>
153
+ <Pressable style={S.modal}>
154
+ <Text style={S.modalTitle}>Reset Identity?</Text>
155
+ <Text style={S.modalBody}>
156
+ This permanently deletes your identity and address.{'\n\n'}
157
+ You will lose access to all pending messages. This cannot be undone.
158
+ </Text>
159
+ <View style={S.modalBtns}>
160
+ <Pressable style={[S.modalBtn, S.modalBtnCancel]} onPress={() => setConfirmReset(false)}>
161
+ <Text style={S.modalBtnText}>Cancel</Text>
162
+ </Pressable>
163
+ <Pressable style={[S.modalBtn, S.modalBtnDanger]} onPress={doReset}>
164
+ <Text style={S.modalBtnText}>Delete</Text>
165
+ </Pressable>
166
+ </View>
167
+ </Pressable>
168
+ </Pressable>
169
+ </Modal>
170
+ </ScrollView>
171
+ );
172
+ }
173
+
174
+ // ── Styles ────────────────────────────────────────────────────────────────────
175
+
176
+ const S = StyleSheet.create({
177
+ root: { flex: 1, backgroundColor: '#0c1218' },
178
+ scroll: { paddingBottom: 60, gap: 12 },
179
+
180
+ header: {
181
+ paddingHorizontal: 16, paddingTop: 56, paddingBottom: 14,
182
+ backgroundColor: '#131d26', borderBottomWidth: 1, borderBottomColor: '#1e3040',
183
+ },
184
+ headerTitle: { color: '#d8ecf8', fontSize: 28, fontWeight: '700' },
185
+
186
+ section: {
187
+ backgroundColor: '#131d26', borderRadius: 14, borderWidth: 1,
188
+ borderColor: '#1e3040', padding: 16, gap: 10, marginHorizontal: 14,
189
+ },
190
+ sectionTitle: { color: '#d8ecf8', fontSize: 15, fontWeight: '700' },
191
+ hint: { color: '#7a9db5', fontSize: 13, lineHeight: 19 },
192
+
193
+ row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 10 },
194
+ rowLeft: { flex: 1, gap: 2 },
195
+ rowLabel: { color: '#7a9db5', fontSize: 12 },
196
+ rowValue: { color: '#d8ecf8', fontSize: 13, fontFamily: 'monospace' },
197
+
198
+ actionBtn: { paddingHorizontal: 10, paddingVertical: 5, borderRadius: 8, backgroundColor: '#0d3550', borderWidth: 1, borderColor: '#1e3040' },
199
+ actionBtnText: { color: '#4fb3e8', fontSize: 12, fontWeight: '600' },
200
+
201
+ inputRow: { flexDirection: 'row', gap: 8, alignItems: 'center' },
202
+ inputFlex: { flex: 1 },
203
+ input: {
204
+ borderWidth: 1, borderColor: '#2a4050', backgroundColor: '#0b1820', color: '#d8ecf8',
205
+ borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14,
206
+ },
207
+ saveBtn: { paddingHorizontal: 14, paddingVertical: 10, borderRadius: 10, backgroundColor: '#1a7fc1' },
208
+ saveBtnText: { color: '#e8f6ff', fontSize: 14, fontWeight: '600' },
209
+
210
+ logRow: { flexDirection: 'row', alignItems: 'center', gap: 14 },
211
+ stepBtn: { width: 36, height: 36, borderRadius: 18, backgroundColor: '#0d3550', borderWidth: 1, borderColor: '#1e3040', alignItems: 'center', justifyContent: 'center' },
212
+ stepBtnText: { color: '#4fb3e8', fontSize: 20, lineHeight: 24, fontWeight: '600' },
213
+ logLabel: { flex: 1, color: '#d8ecf8', fontSize: 14, fontFamily: 'monospace' },
214
+
215
+ dangerSection: { borderColor: '#4a1515' },
216
+ dangerTitle: { color: '#ff7070' },
217
+ dangerBtn: { backgroundColor: '#7a1515', borderRadius: 10, paddingVertical: 12, alignItems: 'center' },
218
+ dangerBtnText: { color: '#ffcccc', fontSize: 14, fontWeight: '600' },
219
+ btnDisabled: { opacity: 0.4 },
220
+
221
+ overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.75)', justifyContent: 'center', alignItems: 'center', padding: 24 },
222
+ modal: { width: '100%', backgroundColor: '#131d26', borderRadius: 16, borderWidth: 1, borderColor: '#4a1515', padding: 20, gap: 14 },
223
+ modalTitle: { color: '#ff7070', fontSize: 18, fontWeight: '700' },
224
+ modalBody: { color: '#d8ecf8', fontSize: 14, lineHeight: 22 },
225
+ modalBtns: { flexDirection: 'row', gap: 10 },
226
+ modalBtn: { flex: 1, borderRadius: 10, paddingVertical: 11, alignItems: 'center' },
227
+ modalBtnCancel: { backgroundColor: '#1a2e40' },
228
+ modalBtnDanger: { backgroundColor: '#7a1515' },
229
+ modalBtnText: { color: '#e8f6ff', fontSize: 14, fontWeight: '600' },
230
+ });
@@ -1,14 +1,15 @@
1
1
  import { Stack } from 'expo-router';
2
2
  import { StatusBar } from 'expo-status-bar';
3
3
  import { LogBox } from 'react-native';
4
+ import { LxmfProvider } from '@/context/LxmfContext';
4
5
 
5
6
  LogBox.ignoreLogs(['Unable to determine event arguments for "onModeChange"']);
6
7
 
7
8
  export default function RootLayout() {
8
9
  return (
9
- <>
10
+ <LxmfProvider>
10
11
  <Stack screenOptions={{ headerShown: false }} />
11
- <StatusBar style="auto" />
12
- </>
12
+ <StatusBar style="light" />
13
+ </LxmfProvider>
13
14
  );
14
15
  }