diff --git a/artifacts/mobile/app.json b/artifacts/mobile/app.json
index 0653863..e27cbea 100644
--- a/artifacts/mobile/app.json
+++ b/artifacts/mobile/app.json
@@ -37,7 +37,8 @@
"expo-web-browser"
],
"extra": {
- "apiDomain": "${EXPO_PUBLIC_DOMAIN}"
+ "apiDomain": "${EXPO_PUBLIC_DOMAIN}",
+ "gitCommitHash": "${EXPO_PUBLIC_GIT_SHA}"
},
"experiments": {
"typedRoutes": true,
diff --git a/artifacts/mobile/app/(tabs)/_layout.tsx b/artifacts/mobile/app/(tabs)/_layout.tsx
index 0b6061f..c744df3 100644
--- a/artifacts/mobile/app/(tabs)/_layout.tsx
+++ b/artifacts/mobile/app/(tabs)/_layout.tsx
@@ -1,11 +1,11 @@
import { BlurView } from "expo-blur";
import { isLiquidGlassAvailable } from "expo-glass-effect";
-import { Tabs } from "expo-router";
+import { Link, Tabs, router } from "expo-router";
import { Icon, Label, NativeTabs } from "expo-router/unstable-native-tabs";
import { SymbolView } from "expo-symbols";
-import { Feather, MaterialCommunityIcons } from "@expo/vector-icons";
+import { Feather, MaterialCommunityIcons, Ionicons } from "@expo/vector-icons";
import React from "react";
-import { Platform, StyleSheet, View, useColorScheme } from "react-native";
+import { Platform, Pressable, StyleSheet, View, useColorScheme } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Colors } from "@/constants/colors";
@@ -13,16 +13,16 @@ import { Colors } from "@/constants/colors";
function NativeTabLayout() {
return (
-
-
+
+
-
-
+
+
-
-
+
+
@@ -39,8 +39,7 @@ function ClassicTabLayout() {
) : isWeb ? (
@@ -61,52 +60,53 @@ function ClassicTabLayout() {
/>
) : (
- ),
- }}
+ ),\
+ }}\
>
(\n \n ({ opacity: pressed ? 0.5 : 1 })}>
+ \n \n \n ),
tabBarIcon: ({ color, size }) =>
isIOS ? (
-
+
) : (
-
- ),
- }}
+
+ ),\
+ }}\
/>
isIOS ? (
-
+
) : (
-
- ),
- }}
+
+ ),\
+ }}\
/>
isIOS ? (
-
+
) : (
-
- ),
- }}
+
+ ),\
+ }}\
/>
);
}
export default function TabLayout() {
- if (isLiquidGlassAvailable()) {
- return ;
- }
- return ;
+ if (isLiquidGlassAvailable()) {\n return (\n \n (\n \n ({ opacity: pressed ? 0.5 : 1 })}>\n \n \n \n ),\n }}\n />\n \n \n \n );\n }
+ return ;\
}
diff --git a/artifacts/mobile/app/_layout.tsx b/artifacts/mobile/app/_layout.tsx
index 53c26c3..4eb88fc 100644
--- a/artifacts/mobile/app/_layout.tsx
+++ b/artifacts/mobile/app/_layout.tsx
@@ -50,6 +50,7 @@ function RootLayoutNav() {
+
);
}
diff --git a/artifacts/mobile/app/settings.tsx b/artifacts/mobile/app/settings.tsx
new file mode 100644
index 0000000..b0f38f5
--- /dev/null
+++ b/artifacts/mobile/app/settings.tsx
@@ -0,0 +1,249 @@
+import { Stack } from 'expo-router';
+import { View, Text, StyleSheet, ScrollView, TextInput, Switch, Pressable, Linking, Platform } from 'react-native';
+import { useState, useEffect } from 'react';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import * as SecureStore from 'expo-secure-store';
+import Constants from 'expo-constants';
+import { useTimmy } from '@/context/TimmyContext';
+import { Ionicons } from '@expo/vector-icons';
+import { ConnectionBadge } from '@/components/ConnectionBadge';
+import { Colors } from '@/constants/colors';
+
+const STORAGE_KEYS = {
+ SERVER_URL: 'settings_server_url',
+ NOTIFICATIONS_JOB_COMPLETION: 'settings_notifications_job_completion',
+ NOTIFICATIONS_LOW_BALANCE: 'settings_notifications_low_balance',
+ NOSTR_PRIVATE_KEY: 'settings_nostr_private_key', // Use SecureStore for this
+};
+
+export default function SettingsScreen() {
+ const { apiBaseUrl, setApiBaseUrl, isConnected, nostrPublicKey, connectNostr, disconnectNostr } = useTimmy();
+ const C = Colors.dark;
+
+ const [serverUrl, setServerUrl] = useState(apiBaseUrl);
+ const [jobCompletionNotifications, setJobCompletionNotifications] = useState(false);
+ const [lowBalanceWarning, setLowBalanceWarning] = useState(false);
+ const [currentNpub, setCurrentNpub] = useState(nostrPublicKey);
+
+ useEffect(() => {
+ // Load settings from AsyncStorage and SecureStore
+ const loadSettings = async () => {
+ const storedServerUrl = await AsyncStorage.getItem(STORAGE_KEYS.SERVER_URL);
+ if (storedServerUrl) {
+ setServerUrl(storedServerUrl);
+ }
+ const storedJobCompletion = await AsyncStorage.getItem(STORAGE_KEYS.NOTIFICATIONS_JOB_COMPLETION);
+ if (storedJobCompletion !== null) {
+ setJobCompletionNotifications(JSON.parse(storedJobCompletion));
+ }
+ const storedLowBalance = await AsyncStorage.getItem(STORAGE_KEYS.NOTIFICATIONS_LOW_BALANCE);
+ if (storedLowBalance !== null) {
+ setLowBalanceWarning(JSON.parse(storedLowBalance));
+ }
+ // Nostr npub is handled by TimmyContext, so we just use the provided nostrPublicKey
+ setCurrentNpub(nostrPublicKey);
+ };
+ loadSettings();
+ }, [nostrPublicKey]);
+
+ // Update apiBaseUrl in context when serverUrl changes and is saved
+ useEffect(() => {
+ if (serverUrl !== apiBaseUrl) {
+ setApiBaseUrl(serverUrl);
+ AsyncStorage.setItem(STORAGE_KEYS.SERVER_URL, serverUrl);
+ }
+ }, [serverUrl, setApiBaseUrl, apiBaseUrl]);
+
+ const handleServerUrlChange = (text: string) => {
+ setServerUrl(text);
+ };
+
+ const toggleJobCompletionNotifications = async () => {
+ const newValue = !jobCompletionNotifications;
+ setJobCompletionNotifications(newValue);
+ await AsyncStorage.setItem(STORAGE_KEYS.NOTIFICATIONS_JOB_COMPLETION, JSON.stringify(newValue));
+ };
+
+ const toggleLowBalanceWarning = async () => {
+ const newValue = !lowBalanceWarning;
+ setLowBalanceWarning(newValue);
+ await AsyncStorage.setItem(STORAGE_KEYS.NOTIFICATIONS_LOW_BALANCE, JSON.stringify(newValue));
+ };
+
+ const handleConnectNostr = async () => {
+ // This will ideally link to a dedicated Nostr connection flow
+ console.log('Connect Nostr button pressed');
+ // For now, simulate connection if not connected
+ if (!currentNpub) {
+ // This is a placeholder. Real implementation would involve generating/importing keys.
+ const simulatedNpub = 'npub1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
+ connectNostr(simulatedNpub, 'private_key_placeholder'); // Pass a placeholder private key
+ setCurrentNpub(simulatedNpub);
+ // In a real app, the private key would be securely stored and managed by the context
+ // For now, just a placeholder to show connected state
+ }
+ };
+
+ const handleDisconnectNostr = async () => {
+ await disconnectNostr();
+ setCurrentNpub(null);
+ };
+
+ const appVersion = Constants.expoConfig?.version || 'N/A';
+ const buildCommitHash = Constants.expoConfig?.extra?.gitCommitHash || 'N/A';
+ const giteaRepoUrl = 'http://143.198.27.163:3000/replit/timmy-tower';
+
+ const openGiteaLink = () => {
+ Linking.openURL(giteaRepoUrl);
+ };
+
+ return (
+
+
+
+ Connection
+
+ Server URL
+
+
+
+
+
+
+ Notifications
+
+ Job Completion Push Notifications
+
+
+
+ Low Balance Warning
+
+
+
+ Identity
+
+ Nostr Public Key
+
+ {currentNpub ? `${currentNpub.substring(0, 10)}...${currentNpub.substring(currentNpub.length - 5)}` : 'Not connected'}
+
+
+
+ {!currentNpub ? (
+ [styles.button, { backgroundColor: C.accent, opacity: pressed ? 0.8 : 1 }]}>
+ Connect Nostr
+
+ ) : (
+ [styles.button, { backgroundColor: C.destructive, opacity: pressed ? 0.8 : 1 }]}>
+ Disconnect Nostr
+
+ )}
+
+
+ About
+
+ App Version
+ {appVersion}
+
+
+ Build Commit Hash
+ {buildCommitHash}
+
+ [styles.linkButton, { opacity: pressed ? 0.8 : 1 }]}>
+
+ View project on Gitea
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.dark.background, // Use background color from Colors
+ },
+ scrollContent: {
+ padding: 20,
+ paddingBottom: 40,
+ },
+ sectionHeader: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: Colors.dark.text,
+ marginTop: 20,
+ marginBottom: 10,
+ },
+ settingItem: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingVertical: 12,
+ borderBottomWidth: 0.5,
+ borderBottomColor: Colors.dark.border,
+ },
+ settingLabel: {
+ fontSize: 16,
+ color: Colors.dark.text,
+ flex: 1,
+ },
+ settingValue: {
+ fontSize: 16,
+ },
+ serverUrlContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 2,
+ },
+ input: {
+ flex: 1,
+ borderWidth: 1,
+ borderColor: Colors.dark.border,
+ borderRadius: 8,
+ padding: 8,
+ fontSize: 14,
+ marginRight: 10,
+ },
+ buttonContainer: {
+ marginTop: 20,
+ alignItems: 'flex-start',
+ },
+ button: {
+ paddingVertical: 10,
+ paddingHorizontal: 15,
+ borderRadius: 8,
+ },
+ buttonText: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ linkButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 15,
+ paddingVertical: 8,
+ },
+ linkButtonText: {
+ marginLeft: 5,
+ fontSize: 16,
+ },
+});
diff --git a/artifacts/mobile/package.json b/artifacts/mobile/package.json
index 397d016..eb860e5 100644
--- a/artifacts/mobile/package.json
+++ b/artifacts/mobile/package.json
@@ -4,7 +4,7 @@
"private": true,
"main": "expo-router/entry",
"scripts": {
- "dev": "EXPO_PACKAGER_PROXY_URL=https://$REPLIT_EXPO_DEV_DOMAIN EXPO_PUBLIC_DOMAIN=$REPLIT_DEV_DOMAIN EXPO_PUBLIC_REPL_ID=$REPL_ID REACT_NATIVE_PACKAGER_HOSTNAME=$REPLIT_DEV_DOMAIN pnpm exec expo start --localhost --port $PORT",
+ "dev": "pnpm exec expo start --localhost --port 8081",
"build": "node scripts/build.js",
"serve": "node server/serve.js",
"typecheck": "tsc -p tsconfig.json --noEmit"
diff --git a/artifacts/mobile/scripts/build.js b/artifacts/mobile/scripts/build.js
index 6cce2d1..9f269b7 100644
--- a/artifacts/mobile/scripts/build.js
+++ b/artifacts/mobile/scripts/build.js
@@ -1,6 +1,6 @@
const fs = require("fs");
const path = require("path");
-const { spawn } = require("child_process");
+const { spawn, execSync } = require("child_process");
const { Readable } = require("stream");
const { pipeline } = require("stream/promises");
@@ -127,6 +127,15 @@ function getExpoPublicReplId() {
return process.env.REPL_ID || process.env.EXPO_PUBLIC_REPL_ID;
}
+function getGitSha() {
+ try {
+ return execSync("git rev-parse HEAD", { cwd: workspaceRoot }).toString().trim();
+ } catch (error) {
+ console.warn("Could not get git commit hash:", error.message);
+ return "unknown";
+ }
+}
+
async function startMetro(expoPublicDomain, expoPublicReplId) {
const isRunning = await checkMetroHealth();
if (isRunning) {
@@ -136,10 +145,12 @@ async function startMetro(expoPublicDomain, expoPublicReplId) {
console.log("Starting Metro...");
console.log(`Setting EXPO_PUBLIC_DOMAIN=${expoPublicDomain}`);
+ const gitSha = getGitSha();
const env = {
...process.env,
EXPO_PUBLIC_DOMAIN: expoPublicDomain,
EXPO_PUBLIC_REPL_ID: expoPublicReplId,
+ EXPO_PUBLIC_GIT_SHA: gitSha,
};
if (expoPublicReplId) {