From b41b5647aa10d22ca83cfd3ba97146681e9f28a3 Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Mon, 9 Feb 2026 00:25:45 +0000
Subject: feat(mobile): Add animated UI feedback to sharing modal (#2427)

* feat(mobile): Add animated UI feedback to sharing modal

---------

Co-authored-by: Claude <noreply@anthropic.com>
---
 apps/mobile/app/sharing.tsx | 165 +++++++++++++++++++++++++++++++-------------
 1 file changed, 116 insertions(+), 49 deletions(-)

(limited to 'apps/mobile/app/sharing.tsx')

diff --git a/apps/mobile/app/sharing.tsx b/apps/mobile/app/sharing.tsx
index 25c9d793..355f32ef 100644
--- a/apps/mobile/app/sharing.tsx
+++ b/apps/mobile/app/sharing.tsx
@@ -1,7 +1,11 @@
 import { useEffect, useRef, useState } from "react";
-import { ActivityIndicator, Pressable, View } from "react-native";
+import { Pressable, View } from "react-native";
+import Animated, { FadeIn } from "react-native-reanimated";
 import { useRouter } from "expo-router";
 import { useShareIntentContext } from "expo-share-intent";
+import ErrorAnimation from "@/components/sharing/ErrorAnimation";
+import LoadingAnimation from "@/components/sharing/LoadingAnimation";
+import SuccessAnimation from "@/components/sharing/SuccessAnimation";
 import { Button } from "@/components/ui/Button";
 import { Text } from "@/components/ui/Text";
 import useAppSettings from "@/lib/settings";
@@ -87,55 +91,14 @@ function SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) {
     }),
   );
 
-  return (
-    <View className="flex flex-row gap-3">
-      <Text variant="largeTitle">Hoarding</Text>
-      <ActivityIndicator />
-    </View>
-  );
+  return null;
 }
 
 export default function Sharing() {
   const router = useRouter();
   const [mode, setMode] = useState<Mode>({ type: "idle" });
 
-  const autoCloseTimeoutId = useRef<number | null>(null);
-
-  let comp;
-  switch (mode.type) {
-    case "idle": {
-      comp = <SaveBookmark setMode={setMode} />;
-      break;
-    }
-    case "alreadyExists":
-    case "success": {
-      comp = (
-        <View className="items-center gap-4">
-          <Text variant="largeTitle">
-            {mode.type === "alreadyExists" ? "Already Hoarded!" : "Hoarded!"}
-          </Text>
-          <Button
-            onPress={() => {
-              router.replace(`/dashboard/bookmarks/${mode.bookmarkId}/info`);
-              if (autoCloseTimeoutId.current) {
-                clearTimeout(autoCloseTimeoutId.current);
-              }
-            }}
-          >
-            <Text>Manage</Text>
-          </Button>
-          <Pressable onPress={() => router.replace("dashboard")}>
-            <Text className="text-muted-foreground">Dismiss</Text>
-          </Pressable>
-        </View>
-      );
-      break;
-    }
-    case "error": {
-      comp = <Text variant="largeTitle">Error!</Text>;
-      break;
-    }
-  }
+  const autoCloseTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
 
   // Auto dismiss the modal after saving.
   useEffect(() => {
@@ -143,14 +106,118 @@ export default function Sharing() {
       return;
     }
 
-    autoCloseTimeoutId.current = setTimeout(() => {
-      router.replace("dashboard");
-    }, 2000);
+    autoCloseTimeoutId.current = setTimeout(
+      () => {
+        router.replace("dashboard");
+      },
+      mode.type === "error" ? 3000 : 2500,
+    );
 
-    return () => clearTimeout(autoCloseTimeoutId.current!);
+    return () => {
+      if (autoCloseTimeoutId.current) {
+        clearTimeout(autoCloseTimeoutId.current);
+      }
+    };
   }, [mode.type]);
 
+  const handleManage = () => {
+    if (mode.type === "success" || mode.type === "alreadyExists") {
+      router.replace(`/dashboard/bookmarks/${mode.bookmarkId}/info`);
+      if (autoCloseTimeoutId.current) {
+        clearTimeout(autoCloseTimeoutId.current);
+      }
+    }
+  };
+
+  const handleDismiss = () => {
+    if (autoCloseTimeoutId.current) {
+      clearTimeout(autoCloseTimeoutId.current);
+    }
+    router.replace("dashboard");
+  };
+
   return (
-    <View className="flex-1 items-center justify-center gap-4">{comp}</View>
+    <View className="flex-1 items-center justify-center bg-background">
+      {/* Hidden component that handles the save logic */}
+      {mode.type === "idle" && <SaveBookmark setMode={setMode} />}
+
+      {/* Loading State */}
+      {mode.type === "idle" && <LoadingAnimation />}
+
+      {/* Success State */}
+      {(mode.type === "success" || mode.type === "alreadyExists") && (
+        <Animated.View
+          entering={FadeIn.duration(200)}
+          className="items-center gap-6"
+        >
+          <SuccessAnimation isAlreadyExists={mode.type === "alreadyExists"} />
+
+          <Animated.View
+            entering={FadeIn.delay(400).duration(300)}
+            className="items-center gap-2"
+          >
+            <Text variant="title1" className="font-semibold text-foreground">
+              {mode.type === "alreadyExists" ? "Already Hoarded!" : "Hoarded!"}
+            </Text>
+            <Text variant="body" className="text-muted-foreground">
+              {mode.type === "alreadyExists"
+                ? "This item was saved before"
+                : "Saved to your collection"}
+            </Text>
+          </Animated.View>
+
+          <Animated.View
+            entering={FadeIn.delay(600).duration(300)}
+            className="items-center gap-3 pt-2"
+          >
+            <Button onPress={handleManage} variant="primary" size="lg">
+              <Text className="font-medium text-primary-foreground">
+                Manage
+              </Text>
+            </Button>
+            <Pressable
+              onPress={handleDismiss}
+              className="px-4 py-2 active:opacity-60"
+            >
+              <Text className="text-muted-foreground">Dismiss</Text>
+            </Pressable>
+          </Animated.View>
+        </Animated.View>
+      )}
+
+      {/* Error State */}
+      {mode.type === "error" && (
+        <Animated.View
+          entering={FadeIn.duration(200)}
+          className="items-center gap-6"
+        >
+          <ErrorAnimation />
+
+          <Animated.View
+            entering={FadeIn.delay(300).duration(300)}
+            className="items-center gap-2"
+          >
+            <Text variant="title1" className="font-semibold text-foreground">
+              Oops!
+            </Text>
+            <Text variant="body" className="text-muted-foreground">
+              Something went wrong
+            </Text>
+          </Animated.View>
+
+          <Animated.View
+            entering={FadeIn.delay(500).duration(300)}
+            className="items-center gap-3 pt-2"
+          >
+            <Pressable
+              onPress={handleDismiss}
+              className="px-4 py-2 active:opacity-60"
+            >
+              <Text className="text-muted-foreground">Dismiss</Text>
+            </Pressable>
+          </Animated.View>
+        </Animated.View>
+      )}
+    </View>
   );
 }
-- 
cgit v1.2.3-70-g09d2

