achorein / expo-share-intent

🚀 Simple share intent in an Expo Native Module
MIT License
119 stars 10 forks source link

Share intent is null when sharing into app while it's killed #54

Closed dr-zr closed 2 months ago

dr-zr commented 2 months ago

Describe the bug I'm in the process of switching from the expo-shared-intent-demo to expo-shared-intent due to the privacy manifest. Since I'm using react-navigation, I've looked at the provided example and managed to successfully build the new app. The intent listener works correctly on iOS & Android when the app is in background, however when I kill the app and try to share text/image or multiple images I get redirected to the home screen.

Environment System: OS: macOS 14.4.1 CPU: (8) arm64 Apple M1 Pro Memory: 74.22 MB / 16.00 GB Shell: version: "5.9" path: /bin/zsh Binaries: Node: version: 20.7.0 path: ~/.nvm/versions/node/v20.7.0/bin/node Yarn: version: 1.22.21 path: ~/.nvm/versions/node/v20.7.0/bin/yarn npm: version: 10.1.0 path: ~/.nvm/versions/node/v20.7.0/bin/npm Watchman: version: 2024.01.22.00 path: /opt/homebrew/bin/watchman Managers: CocoaPods: version: 1.15.2 path: /opt/homebrew/bin/pod SDKs: iOS SDK: Platforms:

Additional context Below are code snippets that I use for expo sharing:

// App.tsx

export default function App() {
    useShareIntent({
        debug: true,
        resetOnBackground: true,
    });

    return (
        <ShareIntentProvider>
            <Main style={{ fontFamily: "OpenSans_400Regular" }} />
        </ShareIntentProvider>
    );
}
// linking config
// AppScreen.ShareIntentScreen = "shareintent"

const PREFIX = createURL("");
const PACKAGE_NAME =
    Constants.expoConfig?.android?.package ||
    Constants.expoConfig?.ios?.bundleIdentifier;

export const linking: LinkingOptions<ReactNavigation.RootParamList> = {
    prefixes: [
        `${Constants.expoConfig?.scheme}://`,
        `${PACKAGE_NAME}://`,
        PREFIX,
    ],
    getStateFromPath(path, config) {
        // REQUIRED FOR iOS FIRST LAUNCH
        if (path.includes(`dataUrl=${getShareExtensionKey()}`)) {
            // redirect to the ShareIntent Screen to handle data with the hook
            console.debug(
                "react-navigation[getStateFromPath] redirect to ShareIntent screen",
            );
            return {
                routes: [
                    {
                        name: AppScreen.ShareIntentScreen,
                    },
                ],
            };
        }
        return getStateFromPath(path, config);
    },
    subscribe(
        listener: (url: string) => void,
    ): undefined | void | (() => void) {
        console.debug("react-navigation[subscribe]", PREFIX, PACKAGE_NAME);
        const onReceiveURL = ({ url }: { url: string }) => {
            if (url.includes(getShareExtensionKey())) {
                // REQUIRED FOR iOS WHEN APP IS IN BACKGROUND
                console.debug(
                    "react-navigation[onReceiveURL] Redirect to ShareIntent Screen",
                    url,
                );
                listener(`${getScheme()}://${AppScreen.ShareIntentScreen}`);
            } else {
                console.debug("react-navigation[onReceiveURL] OPEN URL", url);
                listener(url);
            }
        };
        const shareIntentEventSubscription = addStateListener((event) => {
            // REQUIRED FOR ANDROID WHEN APP IS IN BACKGROUND
            console.debug(
                "react-navigation[subscribe] shareIntentStateListener",
                event.value,
            );
            if (event.value === "pending") {
                listener(`${getScheme()}://${AppScreen.ShareIntentScreen}`);
            }
        });
        const urlEventSubscription = Linking.addEventListener(
            "url",
            onReceiveURL,
        );
        return () => {
            // Clean up the event listeners
            shareIntentEventSubscription.remove();
            urlEventSubscription.remove();
        };
    },
    // https://reactnavigation.org/docs/deep-linking/#third-party-integrations
    async getInitialURL() {
        // REQUIRED FOR ANDROID FIRST LAUNCH
        const needRedirect = hasShareIntent(getShareExtensionKey());
        console.debug(
            "react-navigation[getInitialURL] redirect to ShareIntent screen:",
            needRedirect,
        );
        if (needRedirect) {
            return `${Constants.expoConfig?.scheme}://${AppScreen.ShareIntentScreen}`;
        }
        // As a fallback, do the default deep link handling
        const url = await Linking.getInitialURL();
        return url;
    },
};

My plugin config in app.js:

const supportedTextMimeTypes = ["text/*"];

const supportedFileMimeTypes = [
    "image/png",
    "image/jpeg",
    "image/jpg",
    "image/heic",
    "image/heif",
    "image/gif",
    "application/encrypted",
    "application/CDFV2-encrypted",
    "application/pdf",
    "application/zip",
    "application/msword",
    "application/vnd.ms-office",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    "application/excel",
    "application/vnd.ms-excel",
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    "application/mspowerpoint",
    "application/vnd.openxmlformats-officedocument.presentationml.presentation",
    "application/vnd.ms-powerpoint",
    "application/vnd.ms-powerpointtd>",
    "application/rtf",
    "application/x-iwork-keynote-sffkey",
    "application/vnd.apple.keynote",
    "application/x-iwork-pages-sffpages",
    "application/vnd.apple.pages",
    "application/x-iwork-numbers-sffnumbers",
    "application/vnd.apple.numbers",
];
// ....
[
            "expo-share-intent",
            {
                iosActivationRules: {
                    NSExtensionActivationSupportsText: true,
                    NSExtensionActivationSupportsWebURLWithMaxCount: 1,
                    NSExtensionActivationSupportsWebPageWithMaxCount: 1,
                    NSExtensionActivationSupportsImageWithMaxCount: 10,
                    NSExtensionActivationSupportsFileWithMaxCount: 10,
                },
                androidIntentFilters: [
                    ...supportedTextMimeTypes,
                    ...supportedFileMimeTypes,
                ],
                androidMultiIntentFilters: supportedFileMimeTypes,
            },
        ],

When I share data into my app the debug logs look like this:

 DEBUG  expoShareIntent[scheme] from expoConfig: my-app
 DEBUG  useShareIntent[mount] my-app {"debug": true, "resetOnBackground": true}
 DEBUG  useShareIntent[refresh] null
 DEBUG  useShareIntent[active] refresh intent
 DEBUG  useShareIntent[refresh] null
achorein commented 2 months ago

Welcome to the new package ! :)

first of all you can't use the hook and the provider at the same time, please remove useShareIntent() from your App.tsx and provide options like this :

// App.tsx
export default function App() {
    return (
        <ShareIntentProvider options={{
            debug: true,
            resetOnBackground: true,
        }}>
            <Main style={{ fontFamily: "OpenSans_400Regular" }} />
        </ShareIntentProvider>
    );
}

otherwise there should be one of these log when opening the app (if not in background) :

DEBUG react-navigation[onReceiveURL] Redirect to ShareIntent Screen
DEBUG react-navigation[onReceiveURL] OPEN URL
DEBUG react-navigation[getInitialURL] redirect to ShareIntent screen

can you provide them ?

dr-zr commented 2 months ago

@achorein after removing the hook in App.tsx the logs look like this:

 DEBUG  expoShareIntent[scheme] from expoConfig: my-scheme
 DEBUG  useShareIntent[mount] my-scheme {"debug": true, "resetOnBackground": true}
 DEBUG  useShareIntent[refresh] null
 DEBUG  useShareIntent[active] refresh intent
 DEBUG  useShareIntent[refresh] null
 DEBUG  useShareIntent[to-background] reset intent
 DEBUG  expoShareIntent[scheme] from expoConfig: my-scheme
 DEBUG  react-navigation[getInitialURL] redirect to ShareIntent screen: false
 DEBUG  react-navigation[subscribe] my-scheme:// my-package
 DEBUG  useShareIntent[active] refresh intent
 DEBUG  useShareIntent[refresh] null
achorein commented 2 months ago

🤔 is this for your Android test ? looks good without any issue, just an empty intent...

dr-zr commented 2 months ago

Yes, it is. It's on the dev client, and I've opened the app by sharing a link to it

When the app is killed:

https://github.com/achorein/expo-share-intent/assets/155077894/2c61f754-a64b-4fac-a920-49b1ed991acc

When the app is in the background: https://github.com/achorein/expo-share-intent/assets/155077894/d5dc364c-3736-4326-878b-a2961382783a

Note that the behavior is the same when I test it on a real build

achorein commented 2 months ago

can you check your generated android/app/src/main/AndroidManifest.xml and verify that you have android:launchMode="singleTask" on your MainActivity ?

is this happening only on android ? iOS is ok ?

dr-zr commented 2 months ago
  1. I have it
  2. Unfortunately, this happens on both platforms
<activity android:configChanges="keyboard|keyboardHidden|layoutDirection|locale|orientation|screenLayout|screenSize|uiMode" android:exported="true" android:launchMode="singleTask" android:name="com.myPackageName.MainActivity" android:theme="@style/Theme.App.SplashScreen" android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:scheme="eachat"/>
                <data android:scheme="com.eassistent.untis"/>
                <data android:scheme="exp+ea-chat"/>
            </intent-filter>
            <intent-filter android:autoVerify="true" data-generated="true">
                <action android:name="android.intent.action.VIEW"/>
                <data android:host="komat.easistent.com" android:pathPrefix="/login" android:scheme="https"/>
                <data android:host="komat.easistent.com" android:pathPrefix="/mobileapp" android:scheme="https"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
            <intent-filter android:autoVerify="true" data-generated="true">
                <action android:name="android.intent.action.VIEW"/>
                <data android:host="eassistent.at" android:pathPrefix="/login" android:scheme="https"/>
                <data android:host="eassistent.at" android:pathPrefix="/mobileapp" android:scheme="https"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.SEND"/>
                <data android:mimeType="text/*"/>
                <data android:mimeType="image/png"/>
                <data android:mimeType="image/jpeg"/>
                <data android:mimeType="image/jpg"/>
                <data android:mimeType="image/heic"/>
                <data android:mimeType="image/heif"/>
                <data android:mimeType="image/gif"/>
                <data android:mimeType="application/encrypted"/>
                <data android:mimeType="application/CDFV2-encrypted"/>
                <data android:mimeType="application/pdf"/>
                <data android:mimeType="application/zip"/>
                <data android:mimeType="application/msword"/>
                <data android:mimeType="application/vnd.ms-office"/>
                <data android:mimeType="application/vnd.openxmlformats-officedocument.wordprocessingml.document"/>
                <data android:mimeType="application/excel"/>
                <data android:mimeType="application/vnd.ms-excel"/>
                <data android:mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"/>
                <data android:mimeType="application/mspowerpoint"/>
                <data android:mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation"/>
                <data android:mimeType="application/vnd.ms-powerpoint"/>
                <data android:mimeType="application/vnd.ms-powerpointtd&gt;"/>
                <data android:mimeType="application/rtf"/>
                <data android:mimeType="application/x-iwork-keynote-sffkey"/>
                <data android:mimeType="application/vnd.apple.keynote"/>
                <data android:mimeType="application/x-iwork-pages-sffpages"/>
                <data android:mimeType="application/vnd.apple.pages"/>
                <data android:mimeType="application/x-iwork-numbers-sffnumbers"/>
                <data android:mimeType="application/vnd.apple.numbers"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.SEND_MULTIPLE"/>
                <data android:mimeType="image/png"/>
                <data android:mimeType="image/jpeg"/>
                <data android:mimeType="image/jpg"/>
                <data android:mimeType="image/heic"/>
                <data android:mimeType="image/heif"/>
                <data android:mimeType="image/gif"/>
                <data android:mimeType="application/encrypted"/>
                <data android:mimeType="application/CDFV2-encrypted"/>
                <data android:mimeType="application/pdf"/>
                <data android:mimeType="application/zip"/>
                <data android:mimeType="application/msword"/>
                <data android:mimeType="application/vnd.ms-office"/>
                <data android:mimeType="application/vnd.openxmlformats-officedocument.wordprocessingml.document"/>
                <data android:mimeType="application/excel"/>
                <data android:mimeType="application/vnd.ms-excel"/>
                <data android:mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"/>
                <data android:mimeType="application/mspowerpoint"/>
                <data android:mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation"/>
                <data android:mimeType="application/vnd.ms-powerpoint"/>
                <data android:mimeType="application/vnd.ms-powerpointtd&gt;"/>
                <data android:mimeType="application/rtf"/>
                <data android:mimeType="application/x-iwork-keynote-sffkey"/>
                <data android:mimeType="application/vnd.apple.keynote"/>
                <data android:mimeType="application/x-iwork-pages-sffpages"/>
                <data android:mimeType="application/vnd.apple.pages"/>
                <data android:mimeType="application/x-iwork-numbers-sffnumbers"/>
                <data android:mimeType="application/vnd.apple.numbers"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>
achorein commented 2 months ago

the example works well so I think it's something about your Provider tree.

can you try to add the ShareIntentProvider at the end of your SplashScreenWrapper (when app is ready). you can also try to use the useShareIntentContext() hook in the main page to see if you got something.

dr-zr commented 2 months ago

Thank you for your effort. Guess I'll just have to comment out the code piece by piece until I find the issue.

dr-zr commented 2 months ago

@achorein Just to let you know, I've fixed this issue. The issue was that I've imported Linking from react-native, and not from expo-linking 😅 (auto-import)

Also, when the app is killed on the dev client, share intent won't work (I've copy-pasted your example code to see what am I missing, but even in your example it didn't work). I think it would be nice to add a disclaimer to readme so people don't spend much time doing needles debugging 😅

achorein commented 2 months ago

@dr-zr I don't really understand your feedback, you fixed the "killed" problem with the good import from expo-linking, but it doesn't work on dev-client, so it's not fixed ?

dr-zr commented 2 months ago

@achorein My issue was that the share intent didn't work on the dev client and QA/prod builds when the app was killed.

I commented out all of my code and copy-pasted your code from the example for react-navigation. After copying the code I tried to share a text/file on the dev client but it didn't work. Then I thought sharing an intent in the dev client while in a killed state doesn't work. So I created a QA built to see if it would work. And it did.

So I removed the copy-pasted code, reverted to my code, fixed the wrong Linking import, and created a QA build. I tested the QA build and I was able to receive the shared intent while the app was killed.

To summarize: