From ece68ed078be3f6d66b5dcd7de8ba9853d48be27 Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Mon, 22 Dec 2025 16:36:23 +0200
Subject: feat(mobile): Convert server address editing to modal in mobile app
 (#2290)

* feat: Convert server address editing to modal in mobile app

Changed the server address editing experience from an inline button to a modal dialog. This improves UX by forcing users to explicitly save or cancel their changes rather than forgetting to click a save button.

Changes:
- Created ServerAddressModal component following the CustomHeadersModal pattern
- Updated signin page to use the modal instead of inline editing
- Enhanced settings page to allow changing server address (was previously read-only)
- Added validation and error handling within the modal
- Made the settings page server address clickable with visual feedback

This resolves the issue where users forget to click the save button after editing the server address.

* refactor: Convert server address to screen modal

Changed from React Native Modal to Expo Router screen modal presentation.
This provides a better native experience with proper navigation stack integration.

Changes:
- Created server-address.tsx as a screen route with modal presentation
- Registered the route in root _layout.tsx
- Updated signin.tsx to navigate to the screen modal instead of opening RN modal
- Reverted settings page to original (no server address editing from settings)
- Removed ServerAddressModal component (no longer needed)

Benefits:
- Native modal presentation with proper animations
- Better integration with the navigation stack
- Cleaner separation of concerns

* merge the custom headers inside the server-add screen

* fix the look of the address UI

---------

Co-authored-by: Claude <noreply@anthropic.com>
---
 apps/mobile/app/server-address.tsx | 230 +++++++++++++++++++++++++++++++++++++
 1 file changed, 230 insertions(+)
 create mode 100644 apps/mobile/app/server-address.tsx

(limited to 'apps/mobile/app/server-address.tsx')

diff --git a/apps/mobile/app/server-address.tsx b/apps/mobile/app/server-address.tsx
new file mode 100644
index 00000000..b63aaf4c
--- /dev/null
+++ b/apps/mobile/app/server-address.tsx
@@ -0,0 +1,230 @@
+import { useState } from "react";
+import { Pressable, View } from "react-native";
+import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
+import { Stack, useRouter } from "expo-router";
+import { Button } from "@/components/ui/Button";
+import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView";
+import { Input } from "@/components/ui/Input";
+import PageTitle from "@/components/ui/PageTitle";
+import { Text } from "@/components/ui/Text";
+import useAppSettings from "@/lib/settings";
+import { Plus, Trash2 } from "lucide-react-native";
+import { useColorScheme } from "nativewind";
+
+export default function ServerAddress() {
+  const router = useRouter();
+  const { colorScheme } = useColorScheme();
+  const iconColor = colorScheme === "dark" ? "#d1d5db" : "#374151";
+  const { settings, setSettings } = useAppSettings();
+  const [address, setAddress] = useState(
+    settings.address ?? "https://cloud.karakeep.app",
+  );
+  const [error, setError] = useState<string | undefined>();
+
+  // Custom headers state
+  const [headers, setHeaders] = useState<{ key: string; value: string }[]>(
+    Object.entries(settings.customHeaders || {}).map(([key, value]) => ({
+      key,
+      value,
+    })),
+  );
+  const [newHeaderKey, setNewHeaderKey] = useState("");
+  const [newHeaderValue, setNewHeaderValue] = useState("");
+
+  const handleAddHeader = () => {
+    if (!newHeaderKey.trim() || !newHeaderValue.trim()) {
+      return;
+    }
+
+    // Check if header already exists
+    const existingIndex = headers.findIndex((h) => h.key === newHeaderKey);
+    if (existingIndex >= 0) {
+      // Update existing header
+      const updatedHeaders = [...headers];
+      updatedHeaders[existingIndex].value = newHeaderValue;
+      setHeaders(updatedHeaders);
+    } else {
+      // Add new header
+      setHeaders([...headers, { key: newHeaderKey, value: newHeaderValue }]);
+    }
+
+    setNewHeaderKey("");
+    setNewHeaderValue("");
+  };
+
+  const handleRemoveHeader = (index: number) => {
+    setHeaders(headers.filter((_, i) => i !== index));
+  };
+
+  const handleSave = () => {
+    // Validate the address
+    if (!address.trim()) {
+      setError("Server address is required");
+      return;
+    }
+
+    if (!address.startsWith("http://") && !address.startsWith("https://")) {
+      setError("Server address must start with http:// or https://");
+      return;
+    }
+
+    // Convert headers array to object
+    const headersObject = headers.reduce(
+      (acc, { key, value }) => {
+        if (key.trim() && value.trim()) {
+          acc[key] = value;
+        }
+        return acc;
+      },
+      {} as Record<string, string>,
+    );
+
+    // Remove trailing slash and save
+    const cleanedAddress = address.trim().replace(/\/$/, "");
+    setSettings({
+      ...settings,
+      address: cleanedAddress,
+      customHeaders: headersObject,
+    });
+    router.back();
+  };
+
+  return (
+    <CustomSafeAreaView>
+      <Stack.Screen
+        options={{
+          title: "Server Address",
+          headerRight: () => (
+            <Pressable onPress={handleSave}>
+              <Text className="text-base font-semibold text-blue-500">
+                Save
+              </Text>
+            </Pressable>
+          ),
+        }}
+      />
+      <PageTitle title="Server Address" />
+      <KeyboardAwareScrollView
+        className="w-full"
+        contentContainerClassName="items-center gap-4 px-4 py-4"
+        bottomOffset={20}
+        keyboardShouldPersistTaps="handled"
+      >
+        {/* Error Message */}
+        {error && (
+          <View className="w-full rounded-lg bg-red-50 p-3 dark:bg-red-950">
+            <Text className="text-center text-sm text-red-600 dark:text-red-400">
+              {error}
+            </Text>
+          </View>
+        )}
+
+        {/* Server Address Section */}
+        <View className="w-full">
+          <Text className="mb-2 px-1 text-sm font-medium text-muted-foreground">
+            Server URL
+          </Text>
+          <View className="w-full gap-3 rounded-lg bg-card px-4 py-4">
+            <Text className="text-sm text-muted-foreground">
+              Enter the URL of your Karakeep server
+            </Text>
+            <Input
+              placeholder="https://cloud.karakeep.app"
+              value={address}
+              onChangeText={(text) => {
+                setAddress(text);
+                setError(undefined);
+              }}
+              autoCapitalize="none"
+              keyboardType="url"
+              autoFocus
+              inputClasses="bg-background"
+            />
+            <Text className="text-xs text-muted-foreground">
+              Must start with http:// or https://
+            </Text>
+          </View>
+        </View>
+
+        {/* Custom Headers Section */}
+        <View className="w-full">
+          <Text className="mb-2 px-1 text-sm font-medium text-muted-foreground">
+            Custom Headers
+            {headers.length > 0 && (
+              <Text className="text-muted-foreground"> ({headers.length})</Text>
+            )}
+          </Text>
+          <View className="w-full gap-3 rounded-lg bg-card px-4 py-4">
+            <Text className="text-sm text-muted-foreground">
+              Add custom HTTP headers for API requests
+            </Text>
+
+            {/* Existing Headers List */}
+            {headers.length === 0 ? (
+              <View className="py-4">
+                <Text className="text-center text-sm text-muted-foreground">
+                  No custom headers configured
+                </Text>
+              </View>
+            ) : (
+              <View className="gap-2">
+                {headers.map((header, index) => (
+                  <View
+                    key={index}
+                    className="flex-row items-center gap-3 rounded-lg border border-border bg-background p-3"
+                  >
+                    <View className="flex-1 gap-1">
+                      <Text className="text-sm font-semibold">
+                        {header.key}
+                      </Text>
+                      <Text
+                        className="text-xs text-muted-foreground"
+                        numberOfLines={1}
+                      >
+                        {header.value}
+                      </Text>
+                    </View>
+                    <Pressable
+                      onPress={() => handleRemoveHeader(index)}
+                      className="rounded-md p-2"
+                      hitSlop={8}
+                    >
+                      <Trash2 size={18} color="#ef4444" />
+                    </Pressable>
+                  </View>
+                ))}
+              </View>
+            )}
+
+            {/* Add New Header Form */}
+            <View className="gap-2 border-t border-border pt-4">
+              <Text className="text-sm font-medium">Add New Header</Text>
+              <Input
+                placeholder="Header Name (e.g., X-Custom-Header)"
+                value={newHeaderKey}
+                onChangeText={setNewHeaderKey}
+                autoCapitalize="none"
+                inputClasses="bg-background"
+              />
+              <Input
+                placeholder="Header Value"
+                value={newHeaderValue}
+                onChangeText={setNewHeaderValue}
+                autoCapitalize="none"
+                inputClasses="bg-background"
+              />
+              <Button
+                variant="secondary"
+                onPress={handleAddHeader}
+                disabled={!newHeaderKey.trim() || !newHeaderValue.trim()}
+              >
+                <Plus size={16} color={iconColor} />
+                <Text className="text-sm">Add Header</Text>
+              </Button>
+            </View>
+          </View>
+        </View>
+      </KeyboardAwareScrollView>
+    </CustomSafeAreaView>
+  );
+}
-- 
cgit v1.2.3-70-g09d2

