achorein / expo-share-intent

🚀 Simple share intent in an Expo Native Module
MIT License
128 stars 13 forks source link

[Android] useShareIntent hook has not intent when called on multiple screens #15

Closed TomCorvus closed 4 months ago

TomCorvus commented 4 months ago

Describe the bug In my project, I have a connection screen. If the user is not connected, I display a toast to inform him that he needs to connect to his account, otherwise the app navigates to a screen to manipulate files. useShareIntent is called on login screen and logged user screen. On Android, if I share a file without app opened, the first hook on login intercepts the share intent and hasShareIntent is defined, but when the navigation goes to the logged user screen after the connection, the second hook displays hasShareIntent as null. It will be defined if I share again the file on logged user screen. On iOS, hasShareIntent is defined on both hooks.

If I defined the userShareIntent hook only on logged user screen,hasShareIntent is defined. But I need to use this hook on multiple screens.

Do you know how can I fix that?

To Reproduce I reproduce the problem on basic demo. Just install and init react-navigation, set 2 screens and on each screen, call useShareIntent hook. On the first screen, hasShareIntent is defined, when you navigate to the second screen, useShareIntent hook on it returns nothing.

Additional context

import { Button, Image, StyleSheet, Text, View } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { useShareIntent } from "expo-share-intent";

function HomeScreen({ navigation }: any) {
  const { hasShareIntent, shareIntent, resetShareIntent, error } =
    useShareIntent({
      debug: true,
    });

  console.log("Home");
  console.log(hasShareIntent);

  return (
    <View style={styles.container}>
      <Text>Home</Text>
      <Image
        source={require("./assets/icon.png")}
        style={[styles.logo, styles.gap]}
      />
      <Text style={[styles.gap, { fontWeight: "bold" }]}>
        {hasShareIntent ? "SHARE INTENT FOUND !" : "NO SHARE INTENT DETECTED"}
      </Text>
      {!!shareIntent.text && <Text style={styles.gap}>{shareIntent.text}</Text>}
      {shareIntent?.files?.map((file) => (
        <Image
          key={file.path}
          source={{ uri: file.path }}
          style={[styles.image, styles.gap]}
        />
      ))}
      {!!shareIntent && <Button onPress={resetShareIntent} title="Reset" />}
      <Text style={[styles.error]}>{error}</Text>
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate("Settings")}
      />
    </View>
  );
}

function SettingsScreen() {
  const { hasShareIntent, shareIntent, resetShareIntent, error } =
    useShareIntent({
      debug: true,
    });

  console.log("Settings");
  console.log(hasShareIntent);

  return (
    <View style={styles.container}>
      <Text>Settings</Text>
      <Image
        source={require("./assets/icon.png")}
        style={[styles.logo, styles.gap]}
      />
      <Text style={[styles.gap, { fontWeight: "bold" }]}>
        {hasShareIntent ? "SHARE INTENT FOUND !" : "NO SHARE INTENT DETECTED"}
      </Text>
      {!!shareIntent.text && <Text style={styles.gap}>{shareIntent.text}</Text>}
      {shareIntent?.files?.map((file) => (
        <Image
          key={file.path}
          source={{ uri: file.path }}
          style={[styles.image, styles.gap]}
        />
      ))}
      {!!shareIntent && <Button onPress={resetShareIntent} title="Reset" />}
      <Text style={[styles.error]}>{error}</Text>
    </View>
  );
}

const Stack = createNativeStackNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Settings" component={SettingsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  logo: {
    width: 75,
    height: 75,
    resizeMode: "contain",
  },
  image: {
    width: 200,
    height: 200,
    resizeMode: "contain",
  },
  gap: {
    marginBottom: 20,
  },
  error: {
    color: "red",
  },
});
achorein commented 4 months ago

The hook does not have any persistance system to handle multi component rendering. It just consume the event one time from the native module. For the moment you need to handle it yourself with an useEffect and a react context for example.

Persisting intent it's an interesting feature request

TomCorvus commented 4 months ago

Since it works on iOS, I thought it was a problem on Android. Thanks @achorein, I will try to make it with context.

TomCorvus commented 4 months ago

There is no way to get manually share intent? For example, have a method to get intent like ReceiveSharingIntent.getReceivedFiles This would allow me to retrieve intent on the desired screens.

achorein commented 4 months ago

it's not possible for the moment, the hook should be called on the first screen.

i'm working on a new PR to allow state management, as it needed for clean react-navigation integration.

I will think about the possibility of adding a synchronous method to retrieve the last intent, but this has a lot of disadvantages over the management of the native module which is largely simplified with events

TomCorvus commented 4 months ago

To fix this for now, I created a share intent provider to transmit share intent to all tree:

import React, { ReactNode, createContext, useMemo } from 'react';
import { useShareIntent, ShareIntent } from 'expo-share-intent';

export type ShareIntentContextProps = {
    hasShareIntent:
        | string
        | {
                path: string;
                type: string;
                fileName?: string | undefined;
          }[]
        | null;
    shareIntent: ShareIntent;
    resetShareIntent: () => void;
};

export const ShareIntentContext = createContext<ShareIntentContextProps>({
    hasShareIntent: null,
    shareIntent: { files: null, text: null },
    resetShareIntent: () => null,
});

export const ShareIntentProvider = ({ children }: { children: ReactNode }) => {
    const { hasShareIntent, shareIntent, resetShareIntent } = useShareIntent();

    const shareIntentContext = useMemo(
        () => ({
            hasShareIntent,
            shareIntent,
            resetShareIntent,
        }),
        [hasShareIntent, shareIntent, resetShareIntent]
    );

    return <ShareIntentContext.Provider value={shareIntentContext}>{children}</ShareIntentContext.Provider>;
};
<ShareIntentProvider>
    <App />
</ShareIntentProvider>
import { ShareIntentContext } from './ShareIntentProvider';
const { hasShareIntent, shareIntent, resetShareIntent } = useContext(ShareIntentContext);
achorein commented 4 months ago

fixed in v1.1.0 :)