Repository: artsy/eigen
Pull request #11884: feat(DIA-1252): allow users to share Discover Daily artworks
Author: login: iskounen
Labels: name: Jira Synced
Created: 2025-04-10T20:21:58Z | Merged: 2025-04-11T11:37:22Z

This PR does a few things:

1. It deprecates the rewind button and some "plumbing" that supported it
2. It moves the close button to the left-hand side of the header and changes its icon to a chevron
3. It adds a share button to the right-hand side of the header with tracking events when it is tapped and when the user actually shares
4. It adds a tracking event when the bottom sheet is opened

| iOS | Android |
|----|----------|
| https://github.com/user-attachments/assets/03cec319-72a2-4f26-8dca-9dd12f0a5b5e | https://github.com/user-attachments/assets/6b972f74-eb01-4f0f-b32d-80025b34fbf2 |

### PR Checklist

- [x] I have tested my changes on the following platforms:
  - [x] **Android**.
  - [x] **iOS**.
- [x] I hid my changes behind a **[feature flag]**, or they don't need one.
- [x] I have included **screenshots** or **videos** at least on **Android**, or I have not changed the UI.
- [x] I have added **tests**, or my changes don't require any.
- [x] I added an **[app state migration]**, or my changes do not require one.
- [x] I have documented any **follow-up work** that this PR will require, or it does not require any.
- [x] I have added a **changelog entry** below, or my changes do not require one.

### To the reviewers 👀

- [ ] I would like **at least one** of the reviewers to **run** this PR on the simulator or device.

<details><summary>Changelog updates</summary>

### Changelog updates

<!-- 📝 Please fill out at least one of these sections. -->
<!-- ⓘ 'User-facing' changes will be published as release notes. -->
<!-- ⌫ Feel free to remove sections that don't apply. -->
<!-- • Write a markdown list or just a single paragraph, but stick to plain text. -->
<!-- 📖 eg. `Enable lotsByFollowedArtists` or `Fix phone input misalignment`. -->
<!-- 🤷‍♂️ Replace this entire block with the hashtag `#nochangelog` to avoid updating the changelog. -->
<!-- ⚠️ Prefix with `[NEEDS EXTERNAL QA]` if a change requires external QA -->

#### Cross-platform user-facing changes

- Added the ability to share Discover Daily artworks
- Removed the rewind feature in Discover Daily

#### iOS user-facing changes

-

#### Android user-facing changes

-

#### Dev changes

- Added additional tracking events to Discover Daily

<!-- end_changelog_updates -->

</details>

Need help with something? Have a look at our [docs], or get in touch with us.

[app state migration]: ../blob/main/docs/adding_state_migrations.md
[feature flag]: ../blob/main/docs/developing_a_feature.md
[docs]: ../blob/main/docs/README.md



Diff:
diff --git a/package.json b/package.json
index 22d3c3c06d3..d73d1e7784f 100644
--- a/package.json
+++ b/package.json
@@ -106,7 +106,7 @@
     "braces": "in the resolutions - needed to resolve a security dependency issue - need to remove this when the issue is resolved and packages are updated"
   },
   "dependencies": {
-    "@artsy/cohesion": "4.252.0",
+    "@artsy/cohesion": "4.254.0",
     "@artsy/icons": "3.27.0",
     "@artsy/palette-mobile": "14.0.43",
     "@artsy/to-title-case": "1.1.0",
diff --git a/src/app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryBottomSheet.tsx b/src/app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryBottomSheet.tsx
index 60b05727968..7a1ef735f50 100644
--- a/src/app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryBottomSheet.tsx
+++ b/src/app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryBottomSheet.tsx
@@ -1,3 +1,4 @@
+import { ActionType, ContextModule, OwnerType } from "@artsy/cohesion"
 import { useColor } from "@artsy/palette-mobile"
 import BottomSheet from "@gorhom/bottom-sheet"
 import { InfiniteDiscoveryBottomSheetBackdrop } from "app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryBottomSheetBackdrop"
@@ -8,19 +9,23 @@ import { FC, useEffect, useState } from "react"
 import { Dimensions } from "react-native"
 import { useSafeAreaInsets } from "react-native-safe-area-context"
 import { graphql } from "react-relay"
+import { useTracking } from "react-tracking"
 
 interface InfiniteDiscoveryBottomSheetProps {
   artworkID: string
+  artworkSlug: string
   artistIDs: string[]
 }
 
 export const InfiniteDiscoveryBottomSheet: FC<InfiniteDiscoveryBottomSheetProps> = ({
   artworkID,
+  artworkSlug,
   artistIDs,
 }) => {
   const { bottom } = useSafeAreaInsets()
   const [footerVisible, setFooterVisible] = useState(true)
   const color = useColor()
+  const { trackEvent } = useTracking()
 
   useEffect(() => {
     setFooterVisible(true)
@@ -59,6 +64,12 @@ export const InfiniteDiscoveryBottomSheet: FC<InfiniteDiscoveryBottomSheetProps>
             <InfiniteDiscoveryBottomSheetFooterQueryRenderer artworkID={artworkID} {...props} />
           )
         }}
+        onChange={(index) => {
+          const maxSnapPointIndex = 1
+          if (index === maxSnapPointIndex) {
+            trackEvent(tracks.swipedUp(artworkID, artworkSlug))
+          }
+        }}
       >
         <InfiniteDiscoveryTabs
           artistIDs={artistIDs}
@@ -90,3 +101,13 @@ export const aboutTheWorkQuery = graphql`
     }
   }
 `
+
+const tracks = {
+  swipedUp: (artworkId: string, artworkSlug: string) => ({
+    action: ActionType.swipedUp,
+    context_module: ContextModule.infiniteDiscovery,
+    context_screen_owner_id: artworkId,
+    context_screen_owner_slug: artworkSlug,
+    context_screen_owner_type: OwnerType.infiniteDiscoveryArtwork,
+  }),
+}
diff --git a/src/app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryOnboarding.tsx b/src/app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryOnboarding.tsx
index 792082504b3..fcf07d47e96 100644
--- a/src/app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryOnboarding.tsx
+++ b/src/app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryOnboarding.tsx
@@ -7,7 +7,6 @@ import { MotiView } from "moti"
 import { useEffect, useRef, useState } from "react"
 import { LayoutAnimation, Modal, TouchableWithoutFeedback } from "react-native"
 import LinearGradient from "react-native-linear-gradient"
-import { useSharedValue } from "react-native-reanimated"
 import { SafeAreaView } from "react-native-safe-area-context"
 import { useTracking } from "react-tracking"
 
@@ -39,7 +38,6 @@ export const InfiniteDiscoveryOnboarding: React.FC<InfiniteDiscoveryOnboardingPr
   }
 
   const [isVisible, setIsVisible] = useState(false)
-  const isRewindRequested = useSharedValue(false)
   const [enableTapToDismiss, setEnableTapToDismiss] = useState(false)
 
   const hasInteractedWithOnboarding = GlobalStore.useAppState(
@@ -154,7 +152,6 @@ export const InfiniteDiscoveryOnboarding: React.FC<InfiniteDiscoveryOnboardingPr
                     <Swiper
                       containerStyle={{ flex: 1, transform: [{ scale: 0.8 }] }}
                       cards={artworks}
-                      isRewindRequested={isRewindRequested}
                       onTrigger={() => {}}
                       swipedIndexCallsOnTrigger={2}
                       onNewCardReached={() => {}}
diff --git a/src/app/Scenes/InfiniteDiscovery/Components/Swiper/Swiper.tsx b/src/app/Scenes/InfiniteDiscovery/Components/Swiper/Swiper.tsx
index 1513707b04b..ab66daf65ec 100644
--- a/src/app/Scenes/InfiniteDiscovery/Components/Swiper/Swiper.tsx
+++ b/src/app/Scenes/InfiniteDiscovery/Components/Swiper/Swiper.tsx
@@ -2,7 +2,6 @@ import { InfiniteDiscoveryArtworkCard } from "app/Scenes/InfiniteDiscovery/Compo
 import { AnimatedView } from "app/Scenes/InfiniteDiscovery/Components/Swiper/AnimatedView"
 import { useScreenWidthWithOffset } from "app/Scenes/InfiniteDiscovery/Components/Swiper/useScreenWidthWithOffset"
 import { InfiniteDiscoveryArtwork } from "app/Scenes/InfiniteDiscovery/InfiniteDiscovery"
-import { useBackHandler } from "app/utils/hooks/useBackHandler"
 import { forwardRef, Key, useEffect, useImperativeHandle, useState } from "react"
 import { View, ViewStyle } from "react-native"
 import { Gesture, GestureDetector } from "react-native-gesture-handler"
@@ -11,7 +10,6 @@ import {
   Extrapolation,
   interpolate,
   runOnJS,
-  SharedValue,
   useAnimatedReaction,
   useSharedValue,
   withDelay,
@@ -30,9 +28,8 @@ import {
 
 type SwiperProps = {
   cards: InfiniteDiscoveryArtwork[]
-  isRewindRequested: SharedValue<boolean>
   onNewCardReached?: (key: Key) => void
-  onRewind: (key: Key, wasSwiped?: boolean) => void
+  onRewind: (key: Key) => void
   onSwipe: (swipedKey: Key, nextKey: Key) => void
   containerStyle?: ViewStyle
   cardStyle?: ViewStyle
@@ -50,7 +47,6 @@ export const Swiper = forwardRef<SwiperRefProps, SwiperProps>(
   (
     {
       cards: _cards,
-      isRewindRequested,
       onNewCardReached,
       onRewind,
       onSwipe,
@@ -96,12 +92,6 @@ export const Swiper = forwardRef<SwiperRefProps, SwiperProps>(
       }
     }, [_cards.length])
 
-    useBackHandler(() => {
-      rewindLastCard()
-
-      return true
-    })
-
     // Without this, we are only to rewind a few times
     useEffect(() => {
       if (cards) {
@@ -115,39 +105,6 @@ export const Swiper = forwardRef<SwiperRefProps, SwiperProps>(
       }
     }, [cards.length, numberExtraCardsAdded])
 
-    const rewindLastCard = () => {
-      const hasSwipedCards = _activeIndex.value + 1 < cards.length
-
-      let lastSwipedCardKey = null
-
-      // TODO: clean up this minefield of if-statements
-      if (hasSwipedCards) {
-        lastSwipedCardKey = cards[_activeIndex.value + 1].internalID
-      }
-
-      swipedCardX.value = withTiming(0, { duration: 400, easing: Easing.out(Easing.cubic) }, () => {
-        if (hasSwipedCards) {
-          swipedKeys.value = swipedKeys.value.slice(0, -1)
-          _activeIndex.value = _activeIndex.value + 1
-        }
-        swipedCardX.value = -width
-      })
-
-      if (!!lastSwipedCardKey) {
-        runOnJS(onRewind)(lastSwipedCardKey as Key, false)
-      }
-    }
-    useAnimatedReaction(
-      () => isRewindRequested.value,
-      (current, previous) => {
-        if (current && !previous) {
-          runOnJS(rewindLastCard)()
-
-          isRewindRequested.value = false
-        }
-      }
-    )
-
     const swipeLeftThenRight = (duration: number) => {
       const swipedCardIndex = _activeIndex.value
       const swipedCardKey = cards[swipedCardIndex]?.internalID
diff --git a/src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx b/src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx
index b553a12f426..b3b8de6b668 100644
--- a/src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx
+++ b/src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx
@@ -1,9 +1,9 @@
 import { ActionType, ContextModule, OwnerType } from "@artsy/cohesion"
 import {
-  ArrowBackIcon,
-  CloseIcon,
+  ChevronIcon,
   Flex,
   Screen,
+  ShareIcon,
   Spacer,
   Spinner,
   Text,
@@ -15,6 +15,7 @@ import {
   InfiniteDiscoveryQuery$data,
 } from "__generated__/InfiniteDiscoveryQuery.graphql"
 import { LoadFailureView } from "app/Components/LoadFailureView"
+import { getShareURL } from "app/Components/ShareSheet/helpers"
 
 import { useToast } from "app/Components/Toast/toastHook"
 import { ICON_HIT_SLOP } from "app/Components/constants"
@@ -30,8 +31,8 @@ import { withSuspense } from "app/utils/hooks/withSuspense"
 import { pluralize } from "app/utils/pluralize"
 import { ExtractNodeType } from "app/utils/relayHelpers"
 import { Key, useCallback, useEffect, useMemo, useState } from "react"
-import { useSharedValue } from "react-native-reanimated"
 import { useSafeAreaInsets } from "react-native-safe-area-context"
+import RNShare from "react-native-share"
 import { fetchQuery, graphql, useLazyLoadQuery } from "react-relay"
 import { useTracking } from "react-tracking"
 
@@ -66,8 +67,6 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({
     [artworks, topArtworkId]
   )
 
-  const isRewindRequested = useSharedValue(false)
-
   const insets = useSafeAreaInsets()
 
   useEffect(() => {
@@ -101,15 +100,6 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({
   // TODO: beware! the artworks are being displayed in reverse order
   const unswipedCardIds = artworks.slice(0, currentIndex).map((artwork) => artwork.internalID)
 
-  // TODO: beware! the artworks are being displayed in reverse order
-  const hideRewindButton =
-    !!artworks.length && topArtworkId === artworks[artworks.length - 1].internalID
-
-  const handleBackPressed = () => {
-    isRewindRequested.value = true
-    // TODO: trackEvent(tracks.tappedRewind(artworkToRewind.internalID, artworkToRewind.slug))
-  }
-
   /**
    * The callack for when a card is displayed to the user for the first time.
    * @param key The key of the new card.
@@ -143,16 +133,15 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({
   /**
    * The callback for when a swiped card is brought back.
    * @param key The key of the card that was brought back.
-   * @param wasSwiped True if the card was swiped back, false if it was tapped back.
    */
-  const handleRewind = (key: Key, wasSwiped = true) => {
+  const handleRewind = (key: Key) => {
     const artwork = artworks.find((artwork) => artwork.internalID === key)
 
     if (!artwork) {
       return
     }
 
-    trackEvent(tracks.tappedRewind(artwork.internalID, artwork.slug, wasSwiped ? "swipe" : "tap"))
+    trackEvent(tracks.tappedRewind(artwork.internalID, artwork.slug))
 
     setTopArtworkId(artwork.internalID)
   }
@@ -219,6 +208,33 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({
     goBack()
   }
 
+  const hideShareButton = !topArtwork || !topArtwork.slug || !topArtwork.title
+
+  const handleSharePressed = () => {
+    if (!topArtwork || !topArtwork.slug || !topArtwork.title) {
+      return
+    }
+
+    trackEvent(tracks.tappedShare(topArtwork.internalID, topArtwork.slug))
+
+    const url = getShareURL(`/artwork/${topArtwork.slug}?utm_content=discover-daily-share`)
+    const message = `View ${topArtwork.title} on Artsy`
+
+    RNShare.open({
+      title: topArtwork.title,
+      message: message + "\n" + url,
+      failOnCancel: false,
+    })
+      .then((result) => {
+        if (result.success) {
+          trackEvent(tracks.share(topArtwork.internalID, topArtwork.slug, result.message))
+        }
+      })
+      .catch((error) => {
+        console.error("InfiniteDiscovery.tsx", error)
+      })
+  }
+
   // Get the last 2 artworks from the infinite discovery
   // We are showing the last 2 artworks instead of 2 because we reverse the artworks array
   // Inside the Swiper component
@@ -234,23 +250,23 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({
             title="Discover Daily"
             leftElements={
               <Touchable
-                onPress={handleBackPressed}
-                testID="back-icon"
+                onPress={handleExitPressed}
+                testID="close-icon"
                 hitSlop={ICON_HIT_SLOP}
                 haptic
               >
-                <ArrowBackIcon />
+                <ChevronIcon direction="down" />
               </Touchable>
             }
-            hideLeftElements={hideRewindButton}
+            hideRightElements={hideShareButton}
             rightElements={
               <Touchable
-                onPress={handleExitPressed}
-                testID="close-icon"
+                onPress={handleSharePressed}
+                testID="share-icon"
                 hitSlop={ICON_HIT_SLOP}
                 haptic
               >
-                <CloseIcon fill="black100" />
+                <ShareIcon />
               </Touchable>
             }
           />
@@ -258,7 +274,6 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({
         <Spacer y={1} />
         <Swiper
           cards={artworks}
-          isRewindRequested={isRewindRequested}
           onTrigger={handleFetchMore}
           swipedIndexCallsOnTrigger={2}
           onNewCardReached={handleNewCardReached}
@@ -268,6 +283,7 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({
         {!!topArtwork && (
           <InfiniteDiscoveryBottomSheet
             artworkID={topArtwork.internalID}
+            artworkSlug={topArtwork.slug}
             artistIDs={topArtwork.artists.map((data) => data?.internalID ?? "")}
           />
         )}
@@ -279,8 +295,7 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({
 const InfiniteDiscoveryHeader = () => (
   <Screen.Header
     title="Discover Daily"
-    hideLeftElements
-    rightElements={
+    leftElements={
       <Touchable
         onPress={() => {
           goBack()
@@ -289,9 +304,10 @@ const InfiniteDiscoveryHeader = () => (
         hitSlop={ICON_HIT_SLOP}
         haptic
       >
-        <CloseIcon fill="black100" />
+        <ChevronIcon direction="down" />
       </Touchable>
     }
+    hideRightElements
   />
 )
 
@@ -389,6 +405,7 @@ export const infiniteDiscoveryQuery = graphql`
             internalID @required(action: NONE)
           }
           slug
+          title
         }
       }
     }
@@ -402,6 +419,14 @@ const tracks = {
     context_screen_owner_slug: artworkSlug,
     context_screen_owner_type: OwnerType.infiniteDiscoveryArtwork,
   }),
+  share: (artworkId: string, artworkSlug: string, service: string) => ({
+    action: ActionType.share,
+    context_module: ContextModule.infiniteDiscovery,
+    context_owner_type: OwnerType.infiniteDiscoveryArtwork,
+    context_owner_id: artworkId,
+    context_owner_slug: artworkSlug,
+    service,
+  }),
   swipedArtwork: (artworkId: string, artworkSlug: string) => ({
     action: ActionType.swipedInfiniteDiscoveryArtwork,
     context_module: ContextModule.infiniteDiscovery,
@@ -413,13 +438,20 @@ const tracks = {
     action: ActionType.tappedClose,
     context_module: ContextModule.infiniteDiscovery,
   }),
-  tappedRewind: (artworkId: string, artworkSlug: string, mode: "swipe" | "tap") => ({
+  tappedRewind: (artworkId: string, artworkSlug: string) => ({
     action: ActionType.tappedRewind,
     context_module: ContextModule.infiniteDiscovery,
     context_screen_owner_id: artworkId,
     context_screen_owner_slug: artworkSlug,
     context_screen_owner_type: OwnerType.infiniteDiscoveryArtwork,
-    mode,
+    mode: "swipe",
+  }),
+  tappedShare: (artworkId: string, artworkSlug: string) => ({
+    action: ActionType.tappedShare,
+    context_module: ContextModule.infiniteDiscovery,
+    context_screen_owner_id: artworkId,
+    context_screen_owner_slug: artworkSlug,
+    context_screen_owner_type: OwnerType.infiniteDiscoveryArtwork,
   }),
   tappedSummary: () => ({
     action: ActionType.tappedToast,
diff --git a/yarn.lock b/yarn.lock
index 1c8d0f297e2..731d3b4e7de 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10,10 +10,10 @@
     "@jridgewell/gen-mapping" "^0.3.0"
     "@jridgewell/trace-mapping" "^0.3.9"
 
-"@artsy/cohesion@4.252.0":
-  version "4.252.0"
-  resolved "https://registry.yarnpkg.com/@artsy/cohesion/-/cohesion-4.252.0.tgz#c4aec2ad2c08edc411af26ea51fb900c901dd96d"
-  integrity sha512-cJZR1tt+fUZSQr84FPbyycgmqyAYIi7Nx6Th1DDkB/MHnu8/18sNFl8bCjP3OrJufhu4ojg8AoUGBDMIx3t0uA==
+"@artsy/cohesion@4.254.0":
+  version "4.254.0"
+  resolved "https://registry.yarnpkg.com/@artsy/cohesion/-/cohesion-4.254.0.tgz#28456de19278d781a05e5f8b263f8080436b1953"
+  integrity sha512-LhBZJea4hyBeMPinR4KwN5lyMOzmX0hLXzJLNCRo77ztn5XcaMGW8hmKARe/NGBmuPlMc+sKvoMAgx/x3C5JKA==
   dependencies:
     core-js "3"
 


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

<details>
<summary>Files not reviewed (1)</summary>

* **package.json**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx:137**
* [nitpick] The removal of the 'wasSwiped' parameter results in always tracking the rewind action as 'swipe'. Please confirm that this change is intended and that no distinction is needed between swipe and tap actions.
```
const handleRewind = (key: Key) => {
```
**src/app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryBottomSheet.tsx:67**
* [nitpick] Consider extracting the hard-coded snap point index '1' into a named constant to improve code clarity and maintainability.
```
onChange={(index) => {
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:22:40Z
user: login: iskounen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:26:03Z
user: login: iskounen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:27:17Z
user: login: iskounen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:31:11Z
user: login: MounirDhahri
body: @iskounen great addition, I just left a small comment. I would also strongly suggest to test things out on Android to compare the behaviour between both platforms
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-11T09:04:49Z
user: login: iskounen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T11:28:17Z

Review comments:
diff_hunk: @@ -219,6 +208,29 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({
     goBack()
   }
 
+  const hideShareButton = !topArtwork || !topArtwork.slug || !topArtwork.title
+
+  const handleSharePressed = () => {
+    if (!topArtwork || !topArtwork.slug || !topArtwork.title) {
+      return
+    }
+
+    trackEvent(tracks.tappedShare(topArtwork.internalID, topArtwork.slug))
+
+    const url = getShareURL(`/artwork/${topArtwork.slug}?utm_content=discover-daily-share`)
+    const message = `View ${topArtwork.title} on Artsy`
+
+    RNShare.open({
path: src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx
user: login: Copilot
body: Consider adding error handling for the RNShare.open promise rejection path to gracefully handle potential sharing failures.
created_at: 2025-04-10T20:22:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,6 +64,12 @@ export const InfiniteDiscoveryBottomSheet: FC<InfiniteDiscoveryBottomSheetProps>
             <InfiniteDiscoveryBottomSheetFooterQueryRenderer artworkID={artworkID} {...props} />
           )
         }}
+        onChange={(index) => {
+          const maxSnapPointIndex = 1
+          if (index === maxSnapPointIndex) {
+            trackEvent(tracks.swipedUp(artworkID, artworkSlug))
+          }
+        }}
path: src/app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryBottomSheet.tsx
user: login: iskounen
body: This change fires a tracking event when the user opens the bottom sheet to see artwork data.
created_at: 2025-04-10T20:26:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -115,39 +105,6 @@ export const Swiper = forwardRef<SwiperRefProps, SwiperProps>(
       }
     }, [cards.length, numberExtraCardsAdded])
 
-    const rewindLastCard = () => {
-      const hasSwipedCards = _activeIndex.value + 1 < cards.length
-
-      let lastSwipedCardKey = null
-
-      // TODO: clean up this minefield of if-statements
-      if (hasSwipedCards) {
-        lastSwipedCardKey = cards[_activeIndex.value + 1].internalID
-      }
-
-      swipedCardX.value = withTiming(0, { duration: 400, easing: Easing.out(Easing.cubic) }, () => {
-        if (hasSwipedCards) {
-          swipedKeys.value = swipedKeys.value.slice(0, -1)
-          _activeIndex.value = _activeIndex.value + 1
-        }
-        swipedCardX.value = -width
-      })
-
-      if (!!lastSwipedCardKey) {
-        runOnJS(onRewind)(lastSwipedCardKey as Key, false)
-      }
-    }
-    useAnimatedReaction(
-      () => isRewindRequested.value,
-      (current, previous) => {
-        if (current && !previous) {
-          runOnJS(rewindLastCard)()
-
-          isRewindRequested.value = false
-        }
-      }
-    )
-
path: src/app/Scenes/InfiniteDiscovery/Components/Swiper/Swiper.tsx
user: login: iskounen
body: This change removes the "plumbing" that allowed the parent component to initiate a rewind in this component.
created_at: 2025-04-10T20:27:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -219,6 +208,29 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({
     goBack()
   }
 
+  const hideShareButton = !topArtwork || !topArtwork.slug || !topArtwork.title
+
+  const handleSharePressed = () => {
+    if (!topArtwork || !topArtwork.slug || !topArtwork.title) {
+      return
+    }
+
+    trackEvent(tracks.tappedShare(topArtwork.internalID, topArtwork.slug))
+
+    const url = getShareURL(`/artwork/${topArtwork.slug}?utm_content=discover-daily-share`)
+    const message = `View ${topArtwork.title} on Artsy`
+
+    RNShare.open({
path: src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx
user: login: iskounen
body: I saw that other calls to `RNShare.open` were surrounded by a try-catch block, but I feel like those were added because we set `failOnCancel: true` in some instances.

I added an error handler in case users try to share an artwork with a service that causes an error, but I don't feel too strongly about this change.
created_at: 2025-04-10T20:31:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -115,39 +105,6 @@ export const Swiper = forwardRef<SwiperRefProps, SwiperProps>(
       }
     }, [cards.length, numberExtraCardsAdded])
 
-    const rewindLastCard = () => {
-      const hasSwipedCards = _activeIndex.value + 1 < cards.length
-
-      let lastSwipedCardKey = null
-
-      // TODO: clean up this minefield of if-statements
-      if (hasSwipedCards) {
-        lastSwipedCardKey = cards[_activeIndex.value + 1].internalID
-      }
-
-      swipedCardX.value = withTiming(0, { duration: 400, easing: Easing.out(Easing.cubic) }, () => {
-        if (hasSwipedCards) {
-          swipedKeys.value = swipedKeys.value.slice(0, -1)
-          _activeIndex.value = _activeIndex.value + 1
-        }
-        swipedCardX.value = -width
-      })
-
-      if (!!lastSwipedCardKey) {
-        runOnJS(onRewind)(lastSwipedCardKey as Key, false)
-      }
-    }
-    useAnimatedReaction(
-      () => isRewindRequested.value,
-      (current, previous) => {
-        if (current && !previous) {
-          runOnJS(rewindLastCard)()
-
-          isRewindRequested.value = false
-        }
-      }
-    )
-
path: src/app/Scenes/InfiniteDiscovery/Components/Swiper/Swiper.tsx
user: login: MounirDhahri
body: this is great!
created_at: 2025-04-11T09:01:29Z
author_association: MEMBER
diff_hunk: @@ -219,6 +208,33 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({
     goBack()
   }
 
+  const hideShareButton = !topArtwork || !topArtwork.slug || !topArtwork.title
+
+  const handleSharePressed = () => {
+    if (!topArtwork || !topArtwork.slug || !topArtwork.title) {
+      return
+    }
+
+    trackEvent(tracks.tappedShare(topArtwork.internalID, topArtwork.slug))
+
+    const url = getShareURL(`/artwork/${topArtwork.slug}?utm_content=discover-daily-share`)
path: src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx
user: login: MounirDhahri
body: I assume this url is missing `artsy.net` or `staging.artsy.net`
created_at: 2025-04-11T09:02:52Z
author_association: MEMBER
diff_hunk: @@ -219,6 +208,33 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({
     goBack()
   }
 
+  const hideShareButton = !topArtwork || !topArtwork.slug || !topArtwork.title
+
+  const handleSharePressed = () => {
+    if (!topArtwork || !topArtwork.slug || !topArtwork.title) {
+      return
+    }
+
+    trackEvent(tracks.tappedShare(topArtwork.internalID, topArtwork.slug))
+
+    const url = getShareURL(`/artwork/${topArtwork.slug}?utm_content=discover-daily-share`)
path: src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx
user: login: iskounen
body: For staging I'm seeing that `staging.artsy.net` is being correctly prepended to the URL. This is how we define the URL for other share buttons on artworks, artists, fairs, etc. so I think it's OK.
created_at: 2025-04-11T11:28:17Z
author_association: CONTRIBUTOR

PR comments:
user: login: ArtsyOpenSource
created_at: 2025-04-10T20:25:01Z
body: 
<!--
  0 failure: 
  0 warning: 
  
  1 markdown notices
  DangerID: danger-id-Danger;
-->



### This PR contains the following changes:

- Cross-platform user-facing changes (Added the ability to share Discover Daily artworks,Removed the rewind feature in Discover Daily - iskounen)
- Dev changes (Added additional tracking events to Discover Daily - iskounen)
<p align="right">
  Generated by :no_entry_sign: <a href="https://danger.systems/js">dangerJS</a> against a7c7df1680666d860f1787b403ed0109ea6e2f82
</p>

author_association: CONTRIBUTOR
user: login: JanaeHijaz
created_at: 2025-04-10T21:04:54Z
body: I'm a bit confused because it says "Copy" and not "Share" and it doesn't have any of the standard sharing options that one would expect. Did I miss some context around this?
author_association: CONTRIBUTOR
user: login: iskounen
created_at: 2025-04-11T11:24:45Z
body: @JanaeHijaz the iOS simulator doesn't have all of the share options like a real device would (messages, air drop, slack, etc.). The copy option lets users copy the URL to an artwork and share it however they wish, and also exists on real devices. Check out the screenshot from a real device [here](https://artsy.slack.com/archives/C05EEBNEF71/p1744312320084969?thread_ts=1744303499.997079&cid=C05EEBNEF71).
author_association: CONTRIBUTOR
user: login: iskounen
created_at: 2025-04-11T11:37:14Z
body: @MounirDhahri I tested it out on the Android emulator and it looks OK to me (screen cap in description). I'm eager to have someone test it out on a real device during the QA session.
author_association: CONTRIBUTOR