Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

deeplinking in android detached app fires multiple times when app in background #2128

Closed
aniltirola opened this issue Aug 20, 2018 · 18 comments

Comments

@aniltirola
Copy link

aniltirola commented Aug 20, 2018

Environment

app-target: android
"expo": "^29.0.0",
"expokit": "1.4.0",
"react": "16.3.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-29.0.0.tar.gz"
"exp": 56.0.0

create a android-standalone app with exp detach

Steps to Reproduce

  1. create a minimal project with expo-XDE
  2. set your scheme in app.json to expodeeplinktest
  3. use Linking.addEventListener from react-native and print events to the console
  4. detach your project with exp detach
  5. open your project in android studio
  6. in android studio use "run debug"
  7. now you have on your simulator or on your real device a running standalone-android-app
  8. start your app
  9. push the android-home-button, so that your app is now in the background
  10. test your deeplinks with
    adb shell am start -a android.intent.action.VIEW -d "expodeeplinktest://path/to/my/detailscreen"
  11. app goes to the foreground
  12. event url is fired multiple times

Expected Behavior

  • event url should only be fired one time

Actual Behavior

  • on standalone detached android app: event url fires multiple times
  • on expo-client on android: event url fires only one time (as expected)

Reproducible Demo

@n-sviridenko
Copy link
Contributor

n-sviridenko commented Apr 23, 2019

Any updates on this? @aniltirola

@oriharel
Copy link

oriharel commented Sep 11, 2019

I actually saw that it also fires multiple times in the expo client, however the URL is just the base URL without path/to/my/detailscreen (also without the parameters if there are any). I'm encountering this when working with OAuth and Google

[Update] - my mistake. It doesn't fires multiple times in the expo client.

@schellack
Copy link
Contributor

schellack commented Oct 16, 2019

This is still broken in production for us on Expo SDK 35, expo-web-browser 7.0.1.

@jmzwar
Copy link

jmzwar commented Oct 17, 2019

I can confirm this is broken sdk 35

@jmzwar
Copy link

jmzwar commented Oct 21, 2019

I ended up finding a fix for this - commenting out the following lines in amplify/auth/lib/Auth.js

 // **NOTE** - Remove this in a future major release as it is a breaking change.
 // urlListener_1.default(function (_a) {
 //     var url = _a.url;
 //     _this._handleAuthResponse(url);
 // });

@shemseddine
Copy link

We are experiencing a similar issue. It fires 3 times but only on the android standalone apk. It works well on iOS and also on both iOS and android using the expo client

@aniltirola
Copy link
Author

aniltirola commented Oct 29, 2019

Hi friends,
sorry for not replying a long time here. For me the problem still exists, but it is very easy to write a small workaround. See this simple approach in this gist:

https://gist.github.com/aniltirola/369e08971e18662772d8239bc101fe6f

I think we can close, because it is a small bug, which is easy to workaround.

@schellack
Copy link
Contributor

Not sure that the app crashing is a small bug, but thank you for posting your workaround!

@aniltirola
Copy link
Author

aniltirola commented Oct 30, 2019

Hi @schellack!
Does your app also crash when using the isolated code from my gist? For me the app doesn't crash. It only fires multiple times the deeplink event. But yes - you are right - it is still a bug 😏.

@schellack
Copy link
Contributor

My app restarts. Unfortunately the workaround doesn't seem to help. Perhaps because I'm using AWS Amplify, which fires off multiple requests, and thus gets back multiple deeplinks? I'm still (weeks later) trying to find some way to work around this issue.

But this is still very broken for me. I cannot deploy my app on Android as a result.

@cameronmurphy
Copy link

cameronmurphy commented Nov 25, 2019

I just wrapped my handling function in lodash's debounce with 100ms wait.

Bit of a hack though. I don't know why this issue is closed.

@bryjch
Copy link

bryjch commented Feb 4, 2020

I worked around this by adding a simple short-circuit check in my Linking handler component:

lastUrl = ''

componentDidMount() {
  Linking.addEventListener('url', this._handleOpenURL)
}

_handleOpenURL = ({ url }) => {
  if (this.lastUrl === url) return null

  this.lastUrl = url

  this._navigateDeepLink(url)
}

@duhaime
Copy link

duhaime commented Mar 31, 2020

@aniltirola can you please reopen this issue?

This should be fixed in the expo core, not using downstream solutions like the one you posted.

Those of us that are still experiencing this would be grateful if you could reopen this issue...

@numandev1
Copy link
Contributor

@roger-ngx
Copy link

Linking.removeEventListener seems to do the job

@AtmasApps
Copy link

Just found this page and it seems apropos for me. This happened to me on my deep link standalone android. I'm glad that it fires after following a lot of guides, but it now fires exactly 3 times every single time I click a link. I verified this by my own logging. I also verified using the same logging that the listener in question is definitely only being registered once (thinking on the off chance it was getting triple registered by clones of the component, for example). So this bug seems alive and well. Expo SDK 42 (42.0.0).

@soullivaneuh
Copy link

Have the same issue, here is a complete code sample from #14550:

export const CreditCardAdd: FC<CreditCardAdd> = ({
  onAdd,
}) => {
  const { client, handleError } = useClient();
  const [number, setNumber] = useState<string>('');
  const [expirationDate, setExpirationDate] = useState<string>('');
  const [csv, setCsv] = useState<string>('');

  const [ready, setReady] = useState<boolean>(false);
  const [loading, setLoading] = useState<boolean>(false);

  const creditCardRef = useRef<CreditCardsData>();
  const registrationDataRef = useRef<string>();
  const ipAddressRef = useRef<string>();
  const headersRef = useRef<any>();

  const handleCreditCardRegistrationError = (error: FeathersError): void => {
    setLoading(false);
    if (error.code === 402 || error.message === 'credit-cards.register.failure') {
      Alert.alert(
        "Impossible d'enregistrer la carte",
        'Merci de vérifier les données saisies ainsi que la validité de la carte.'
        + '\nSi le problème persiste, contactez-nous.',
      );
    } else {
      handleError(error);
    }
  };

  const urlEventHandler = async (event): Promise<void> => {
    const parsedUrl = queryString.parseUrl(event.url);

    // Works on iOS only and cause a crash on Android will opening an another browser instance.
    if (Platform.OS === 'ios') {
      console.log('WebBrowser.dismissBrowser');
      await WebBrowser.dismissBrowser();
    }

    if (
      parsedUrl.url.endsWith('/headers')
    ) {
      console.log('headers');
      headersRef.current = parsedUrl.query;

      // We have to set a dirty workaround to the "already opened browser" issue.
      setTimeout(async () => {
        await WebBrowser.openBrowserAsync(
          `${Constants.manifest?.extra?.apiEndpoint
          }/browser.html?redirectUrl=${
            Linking.createURL('browser-info')}`,
        );
      }, 1000);
    }

    if (
      parsedUrl.url.endsWith('/browser-info')
    ) {
      console.log('browser-info');
      client.service('credit-cards')
        .patch(creditCardRef.current._id, {
          registrationId: creditCardRef.current.registrationId,
          registrationData: registrationDataRef.current,
          registrationIpAddress: ipAddressRef.current,
          // @ts-expect-error Unable to set the type of the parsed query.
          registrationBrowserInfo: {
            ...parsedUrl.query,
            acceptHeader: headersRef.current.accept,
          },
          secureModeRedirectUrl: Linking.createURL('paybox-3ds', {
            queryParams: { creditCardId: creditCardRef.current._id },
          }),
        })
        .then(ResultHelpers.toOne)
        .then((result) => WebBrowser.openBrowserAsync(result.secureModeUrl, {
          // @see https://github.com/expo/expo/issues/8072#issuecomment-621173298
          showInRecents: true,
        }))
        .catch((error) => {
          handleCreditCardRegistrationError(error);
        });
    }

    if (
      parsedUrl.url.endsWith('/paybox-3ds')
      && typeof parsedUrl.query.creditCardId === 'string'
      && typeof parsedUrl.query.transactionId === 'string'
    ) {
      console.log('paybox-3ds');
      client.service('credit-cards')
        .patch(
          parsedUrl.query.creditCardId,
          {
            transactionId: parsedUrl.query.transactionId,
          },
        )
        .then(ResultHelpers.toOne)
        .then((result) => onAdd(result._id))
        .catch(handleCreditCardRegistrationError);
    }
  };

  useFocusEffect(
    useCallback(() => {
      Linking.addEventListener('url', urlEventHandler);

      return () => Linking.removeEventListener('url', urlEventHandler);
    }, []),
  );

  const handleSubmit: ButtonProps['onPress'] = () => {
    const expirationDateRaw = expirationDate.replace('/', '');
    const numberRaw = number.replace(/\s/g, '');
    setLoading(true);
    client.service('credit-cards').create({
      lastDigits: numberRaw.slice(numberRaw.length - 4),
      expirationDate: expirationDateRaw,
    })
      .then(ResultHelpers.toOne)
      .then(
        (result) => {
          const cardValidationData = {
            accessKeyRef: result.accessKey,
            data: result.preRegistrationData,
            cardNumber: numberRaw,
            cardExpirationDate: expirationDateRaw,
            cardCvx: csv,
          };

          const formBody = Object.keys(cardValidationData)
            .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(cardValidationData[key])}`).join('&');

          creditCardRef.current = result;

          return Promise
            .all([
              fetch(result.registrationUrl, {
                method: 'POST',
                body: formBody,
                headers: {
                  'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
                },
              })
                .then((registrationResult) => registrationResult.text()),
              fetch('https://api.ipify.org').then((ipResult) => ipResult.text()),
            ])
            .then(async ([registrationData, ipAddress]) => {
              registrationDataRef.current = registrationData;
              ipAddressRef.current = ipAddress;

              await WebBrowser.openBrowserAsync(
                `${Constants.manifest?.extra?.apiEndpoint
                }/headers?redirectUrl=${
                  Linking.createURL('headers')}`,
              );
            });
        },
      )
      .catch(handleCreditCardRegistrationError);
  };

  const handleNumberChange = (newNumber: string): void => {
    setNumber(newNumber.length > number.length
      ? newNumber.replace(/\s/g, '').split(/(.{4})/).filter((x) => x).join(' ')
      : newNumber);
  };

  const handleExpirationDateChange = (newExpirationDate: string): void => {
    setExpirationDate(newExpirationDate.length > expirationDate.length
      ? newExpirationDate.replace(/\//g, '').split(/(.{2})/).filter((x) => x).join('/')
      : newExpirationDate);
  };

  useEffect(() => setReady(
    Boolean(number) && number.length === 19
    && Boolean(expirationDate) && expirationDate.length === 5
    && Boolean(csv) && csv.length === 3,
  ), [number, expirationDate, csv]);

  return (
    <ScrollView>
      <Spacer />
      <Input
        label="Numéro de carte"
        value={number}
        onChangeText={handleNumberChange}
        keyboardType="number-pad"
        autoFocus
        maxLength={19}
        editable={!loading}
      />
      <View style={{ flexDirection: 'row' }}>
        <Input
          label="MM/AA"
          value={expirationDate}
          onChangeText={handleExpirationDateChange}
          keyboardType="number-pad"
          maxLength={5}
          editable={!loading}
        />
        <Input
          label="Code de sécurité (CVV)"
          value={csv}
          onChangeText={setCsv}
          keyboardType="number-pad"
          editable={!loading}
        />
      </View>
      <Button
        title="Valider"
        onPress={handleSubmit}
        loading={loading}
        disabled={!ready}
      />
    </ScrollView>
  );
};

@aniltirola I read your gist, but I honestly don't see what to do comparing to my use case.

I will try @bryjch's workaround which look simpler. May you add your feedback about this method?

Also, I have to agree with @duhaime: This sneaky bug happens only on standalone android app, which is a real pain to identify, debug and fix.

This expo issue should be re-opened waiting for a real solution provided by expo itself.

May you reconsider its status?

Regards

@soullivaneuh
Copy link

@aniltirola Your workaround is indeed working, thanks for that. Here my diff using hooks:

diff --git a/src/components/payment/CreditCardAdd.tsx b/src/components/payment/CreditCardAdd.tsx
index 66d1163..6f082b3 100644
--- a/src/components/payment/CreditCardAdd.tsx
+++ b/src/components/payment/CreditCardAdd.tsx
@@ -55,6 +55,14 @@ export const CreditCardAdd: FC<CreditCardAdd> = ({
   };
 
   const urlEventHandler = async (event): Promise<void> => {
+    // @see https://github.com/expo/expo/issues/2128#issuecomment-547418161
+    const now = Date.now();
+    const elapsed = now - global.timestampLastDeepLinkEvent;
+    if (elapsed < 1000) {
+      return;
+    }
+    global.timestampLastDeepLinkEvent = now;
+
     const parsedUrl = queryString.parseUrl(event.url);
 
     // Works on iOS only and cause a crash on Android will opening an another browser instance.
@@ -128,6 +136,7 @@ export const CreditCardAdd: FC<CreditCardAdd> = ({
   };
 
   useEffect(() => {
+    global.timestampLastDeepLinkEvent = -1;
     Linking.addEventListener('url', urlEventHandler);
 
     return () => Linking.removeEventListener('url', urlEventHandler);

However, relying on an estimated "time to spawn" is quite dangerous and definitely not a solution to this bug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests