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

Can this be used on multiple screens? #17

Open
alexandrumic opened this issue Oct 6, 2020 · 11 comments
Open

Can this be used on multiple screens? #17

alexandrumic opened this issue Oct 6, 2020 · 11 comments

Comments

@alexandrumic
Copy link

Hello,

I have a React Navigation in place and I'm using this library to show a tour for the first tab.

Is there a way to use this, when switching the tab, or go into a new Screen, to start another tour in that specific screen?

@Arun-paramasivam
Copy link

Hi,

Can anyone share code on how to achieve this multiscreen tour. In my case I have two bottom tabs(A and B), the tour goes from one tab(A) to next tab(B). My tour sequence is like A->B->A.
(My navigation container has drawer navigator which has two bottom tab screens)
while moving from B to A, i'll tour drawer too. So when i start the tour, I am initiating tour from A then goes to B and then to drawer and when coming back to A the popup doesnt show up inside A and it goes to B screen automatically and shows the initial step1 in B screen. But if i start the tour from B screen it works as it should.

eventEmitter.on('stepChange', (value) => { if(value){ if(value.text == 'step1' || value.text == 'step2'){ setTimeout(() => { navigation.closeDrawer() }, 500) navigation.navigate('B') }else if(value.text == 'step3' || value.text == 'step4'){ setTimeout(() => { navigation.openDrawer() }, 500) } else if(value.text == 'step5' || value.text == 'step6'){ setTimeout(() => { navigation.closeDrawer() }, 500) navigation.navigate('A') } } })

@patrickspafford
Copy link

Here's something you can try.

  1. Wrap the contents of your app with the TourGuideProvider.
const App = () => (
  <TourGuideProvider>
    <AppContents />
  </TourGuideProvider>
)
  1. Inside the AppContents (or your equivalent), insert the tour useEffect hooks. We will come back to this later.
// AppContents.js
...
const {
    canStart,
    start,
    eventEmitter,
  } = useTourGuideController()

useEffect(() => {
    if (canStart) {
      start()
    }
  }, [canStart])
...
  1. To trigger navigation outside of a react component, follow this link, which will show you how to create a ref to your navigator and then create a navigation service which you can use anywhere if imported.

Note: If you are using Redux navigation on top of a Stack Navigator, the Navigation service does not work without some modifications.

// This is my package.json, no need to have these exact versions, but just including it for clarity.
{
    "react-navigation": "^4.4.0",
    "react-navigation-redux-helpers": "^2.0.6",
    "react-navigation-stack": "^1.10.3",
    "react-redux": "^6.0.0",
    "redux": "^4.0.1",
    "redux-persist": "^5.4.0",
    "redux-persist-seamless-immutable": "^2.0.0",
    "redux-saga": "^1.0.0",
    "reduxsauce": "^1.1.0",
    "rn-tourguide": "^2.7.1",
}

You might have to do something like below.

...
function navigate(routeName, params) {
  _container.props.dispatch(
    NavigationActions.navigate({
      type: 'Navigation/NAVIGATE',
      routeName,
      params,
    }),
  )
}

function getCurrentRoute() { // returns the name of the current screen (defined in your stack navigator)
  if (!_container || !_container.props.state) {
    return null
  }
  const { routes, index } = _container.props.state
  return routes[index].routeName || null
}
...
  1. Create a new file tour.js. In this file, define an object for mapping screen names to the next screen.
// tour.js

const tour = {
  screenNameA: 'screenNameB',
  screenNameB: 'screenNameC',
  ...
}

export default tour
  1. Back in your App Contents, we can now make a few changes. We add a dependency to the useEffect so that when the route changes, we start a new tour for that screen. When the tour for a screen ends, if we have a key-value pair in our tour.js, then we automatically navigate to that screen and begin that screen-specific tour.

A major plus is that we do not have to refactor our class-based components into functional components, and we do not have to insert useEffect hooks into every screen.

Make sure that your TourGuideZones are indexed from 1 on each screen, since it will be seen as a new tour by this package.

// AppContents.js
import NavigatorService from './services/navigator' // The path to that file will of course vary
import tour from './services/tour'
...
useEffect(() => {
    if (canStart) {
      start()
    }
  }, [canStart, NavigatorService.getCurrentRoute()]) // we change this

useEffect(() => { // we add this hook
    eventEmitter.on('stop', () => { // When the tour for that screen ends, navigate to the next screen if it exists.
     const nextScreen = tour[NavigatorService.getCurrentRoute()]
      if (nextScreen) {
        NavigatorService.navigate(nextScreen)
      }
    })
    return () => eventEmitter.off('*', null)
  }, [])
...
  1. Extra detail: I recommend making a Custom tooltip component so that the end of each screen says something other than "Finish". You might consider changing this to "Ok" by default and then to "Finish" when it's the last screen. And you could use the navigation service to check whether the current route is the last screen in your tour sequence.

@ThallyssonKlein
Copy link

@patrickspafford Shouldn't simply adding another TourGuideZone on the other screen work?

@patrickspafford
Copy link

patrickspafford commented Dec 6, 2020

@ThallyssonKlein Probably not in the way that you'd find useful, but you are correct that the provider is made available to all its children.

To my knowledge, there's nothing in this library that would automatically trigger a segue to the next screen, which was part of the idea here. This method automatically transitions to the next page when the tour for that screen ends and gives you the option to hook into different points of the tour to trigger events that you might dispatch to the Redux store. It doesn't require you to add the tour hooks to every screen that's part of the tour (That would be annoying). All you have to do is add the TourGuideZones in the render/return methods once you get it set up in the root component (shown in my other comment).

Let me give you an example of dispatching actions at different points in the tour, since I haven't shown that.

// tour.js
...
screenNameA: {
    nextScreen: 'screenNameB',
    actionOnLoad: () => storeData(`@screenNameA`), // save to local storage that this screen has been toured
    actions: {
      1: () => { // at step 1 of the tour on screenNameA, dispatch these actions
        store.dispatch(action1a)
        store.dispatch(action1b)
      },
      2: () => {
        store.dispatch(action2a)
        store.dispatch(action2b)
      }
  }
...

The useEffect hooks in the root container need to be modified to make use of the extra parts above, though.

Does that make sense?

@ThallyssonKlein
Copy link

I cannot implement Redux in my project. Is there a way to do this using context API?

@ThallyssonKlein
Copy link

ThallyssonKlein commented Dec 7, 2020

The problem here is that this library does not recognize the step on the other screen as a total part of the tutorial.

I created a context with purpose to start the tutorial again on the other screen

import React, { createContext, useState, useContext } from 'react';

export const TutorialContext = createContext();

import { useTourGuideController } from 'rn-tourguide';

export default function TutorialContextProvider({children}){
    const [stepOneVisible, setStepOneVisible] = useState(true);
    const {
        start
    } = useTourGuideController();

    const startTutorials = _ => {
        setStepOneVisible(false);
        start();
    }

    return <TutorialContext.Provider value={{startTutorials, stepOneVisible, start}}>
                {children}
           </TutorialContext.Provider>
}

My App.js

...
 <TourGuideProvider>
          <TutorialContextProvider>
              <NavigationContainer linking={linking}>
                  <Stack.Navigator initialRouteName="Splash">
                    <Stack.Screen name="Splash" component={SplashScreen} options={{headerShown: false}}/>
                    <Stack.Screen name="Start" component={StartScreen} options={{headerShown: false}}/>
                    <Stack.Screen name="Home" component={Home} options={{headerShown: false}}/>
                    <Stack.Screen name="TempScreen" component={TempScreen} options={{headerShown: false}}/>
                    <Stack.Screen name="NewEventScreen" component={NewEventScreen} options={{headerShown: false}}/>
                    <Stack.Screen name="EventDetailScreen" component={EventDetailScreen} options={{headerShown: false}}/>
                    <Stack.Screen name="EventTutorial" component={EventTutorial} options={{headerShown: false}}/>
                    <Stack.Screen name="ManageSubscriptionsScreen" component={ManageSubscriptionsScreen} options={{headerShown: false}}/>
                  </Stack.Navigator>
                </NavigationContainer>
          </TutorialContextProvider>
        </TourGuideProvider>
...
  • On the first screen I have two TourGuideZone tags (with id 1 and 2 respectively) and a button that goes in context and calls startTutorials. When starting the tutorial, the library does not identify that there is a third step on another screen, but so far so good. There is a part of the tutorial that the user needs to click on a component that will take him to the next screen with this third step.
  • On this next screen, where there is the third step, I call startTutorials again in context, but the steps that appear are those of the previous screen, focusing on the wrong location.

I confess that I am having trouble understanding your solution using Redux, and whenever I tried to implement this project I had difficulties. It is too simple a project for Redux. It's like killing a cockroach with a cannon shot. I want to quickly develop this tutorial.

Edited

I tested the code again, and noticed that on the screen where the third step is present, the behavior has something that I didn't pay attention to. Yes, the steps of the previous screen appear, but on this new screen the tutorial has 3 steps and the last one appears, but the behavior I need is to ignore the steps on the other screen.

@Voltron369
Copy link

Just started working on the same thing, I think you can pass a step number to start and it will start there

@Voltron369
Copy link

Voltron369 commented Dec 7, 2020

Using context, no redux

  in my main context provider:

  const [step, setStep] = useState(0);

useEffect(() => {
    ...
    eventEmitter.on('stepChange', step => {
         const s = step?step.order:0
         console.log(`tutorial step ${s}`);
         setStep(s)
     })
    return () => eventEmitter.off('*', null)
  }, [])

on the second screen:

const isFocused = useIsFocused();
const [mounted, setMounted] = useState(false);

useEffect(() => {
  setMounted(true)
  return () => {setMounted(false)}
},[]);

useEffect(() => {
    if (isFocused && mounted && canStart && step === 4) {
        stop()
        console.log('restarting at step 5')
        start(5)
    }
  }, [canStart, step, start, stop, isFocused, mounted])

When the user clicks on the Tab Icon, the tutorial continues. The "previous" button is broken though. It needs to navigate back to the previous tab, right now it just goes back to step 5. Will probably write a custom tooltip component, hopefully one that can take props from the TourGuideZone, so it can be generic.

@ThallyssonKlein
Copy link

It worked perfectly, thanks.

@Voltron369

@ThallyssonKlein
Copy link

ThallyssonKlein commented Dec 17, 2020

@Voltron369

I'm almost done with the tutorial on my APP. But this last step complicated things. I will describe the scenario to see if you can think of a solution.

The component below is a component that is present on the second screen. This is where the steps on the second screen appear. And there is a button that when clicked, changes the context. This change consists of hiding itself and creating 2 new components on the screen.

import React, { useEffect, useContext, useState } from 'react';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
} from 'react-native-reanimated';
import { StyleSheet, View, Image, Text } from 'react-native';

import Colors from '../../../Colors';
import Strings from '../../../Strings';

import { TourGuideZone } from 'rn-tourguide';

const styles = StyleSheet.create({
    column : {
        flexDirection : "column",
        alignItems : "center",
        justifyContent: "center",
        backgroundColor: Colors.primaryShade2,
        textAlign : "center",
        padding : 50,
        margin : 20,
        borderRadius : 50
    },
    row : {
      flexDirection: "row",
      alignItems: "center",
      justifyContent: "center"
    },
    imageStyle : {
        resizeMode: "contain",
        width: 70,
        height: 70,
        borderRadius: 50
    }
});

import { TutorialContext } from '../../contexts/TutorialContext';

export default Compared = props => {
    const { start, step, stop, canStart, cards, setCards } = useContext(TutorialContext);
    const [started, setStarted] = useState(false);
    const [mounted, setMounted] = useState(false);
    const [animationRunned, setAnimationRunned] = useState(false);
    const [visible, setVisible] = useState(true);

    useEffect(() => {
        setMounted(true)
        return () => {setMounted(false)}
    },[]);

    useEffect(() => {
        if (mounted && canStart && step === 0 && animationRunned && !started) {
            stop()
            start(3)
            setStarted(true);
        }
    }, [canStart, step, start, stop, mounted, animationRunned]);

    const titlePosition = useSharedValue(300);

    const animatedStyles = useAnimatedStyle(_ => {
        return {
          transform: [{translateY : titlePosition.value}]
        }
    });

    useEffect(() => {
        titlePosition.value = withTiming(0, {duration: 1000});
        setTimeout(_ => {
            setAnimationRunned(true);
        }, 1000);
    }, []);
    
    const separateAction = _ => {
       setVisible(false);
       let array = [
            <Alone photo={props.photo1}
                   name={props.name1}/>,
            <Alone photo={props.photo2}
                   name={props.name2}/>
       ];
       setCards([...cards, ...array]);
    }

    console.log("TEXTO SEPARATE");
    console.log(Strings.manageSubscriptions.separate);

    return <TourGuideZone    
                zone={3}
                text={Strings.manageSubscriptions.guide3}
                borderRadius={16}
                labels={{skip : Strings.manageSubscriptions.skipLabel, next : Strings.manageSubscriptions.nextLabel}}>
                    {visible &&  <Animated.View style={[styles.column, animatedStyles]}>
                            <View style={styles.row}>
                                <View>
                                    <Image source={{uri: props.photo1}} 
                                    style={[styles.imageStyle, {marginRight: 50}]}/>
                                    <Text>{props.name1}</Text>
                                </View>
                                <Text style={{borderRadius: 50, backgroundColor: "#FD75C7", padding: 10, color: Colors.primaryShade3, textAlign : "center"}}>{props.chance}%{"\n"}{Strings.manageSubscriptions.matchPercent}</Text>
                                <View>
                                    <Image source={{uri: props.photo2}} 
                                        style={[styles.imageStyle, {marginLeft: 50}]}/>
                                    <Text style={{textAlign: "right"}}>{props.name2}</Text>    
                                </View>
                            </View>

                            <TourGuideZone zone={4}
                                           text={Strings.manageSubscriptions.guide4}
                                           borderRadius={16}
                                           labels={{skip : Strings.manageSubscriptions.skipLabel, next : Strings.manageSubscriptions.nextLabel}}>
                                    <Button title={Strings.manageSubscriptions.separate}
                                            onPress={_ => separateAction()}/>       
                            </TourGuideZone> 
                </Animated.View>}
            </TourGuideZone>
}

The Alone Component:

import React, { useEffect, useState, useContext } from 'react';

import Animated, {
    useSharedValue,
    useAnimatedStyle,
    withTiming
} from 'react-native-reanimated';
import { StyleSheet, View, Image, Text } from 'react-native';

import Colors from '../../../Colors';

const styles = StyleSheet.create({
    column : {
      flexDirection : "column",
      alignItems : "center",
      justifyContent: "center",
      textAlign : "center",
      padding : 50,
      margin : 20,
      borderRadius : 50
    },
    row : {
      flexDirection: "row",
      alignItems: "center",
      justifyContent: "center"
    },
    imageStyle : {
        resizeMode: "contain",
        width: 70,
        height: 70,
        borderRadius: 50
    },
    background1 : {
      backgroundColor: Colors.primaryShade2
    },
    background2 : {
       backgroundColor: "rgba(81, 91, 115, 0.5)"
    }
});

// import Button from '../components/Button';

import Strings from '../../../Strings';

import { TourGuideZone } from 'rn-tourguide';
import { TutorialContext } from '../../contexts/TutorialContext';

export default Alone = props => {
    const [selected, setSelected] = useState(false);
    const titlePosition = useSharedValue(300);
    const { cards } = useContext(TutorialContext);

    const animatedStyles = useAnimatedStyle(_ => {
        return {
          transform: [{translateY : titlePosition.value}]
        }
    });

    useEffect(() => {
        titlePosition.value = withTiming(0, {duration: 1000});
    }, []);

    const background = (!selected) ? styles.background1 : styles.background2;

    const CompareButton = _ => {
        if(props.keyI === 1 && cards.length > 3){
            return <TourGuideZone zone={5}
                                  text={Strings.manageSubscriptions.guide5}
                                  borderRadius={16}
                                  labels={{skip : Strings.manageSubscriptions.skipLabel, next : Strings.manageSubscriptions.nextLabel}}>
                          <Button title={Strings.manageSubscriptions.compare}
                                  onPress={ _ => {
                                                    props.select(props.keyI);
                                                    setSelected(true);
                                          }
                                  }
                          />
                  </TourGuideZone>
        }else if(props.keyI === 2 && cards.length > 3){
          return <TourGuideZone zone={5}
                                text={Strings.manageSubscriptions.guide6}
                                borderRadius={16}
                                labels={{skip : Strings.manageSubscriptions.skipLabel, next : Strings.manageSubscriptions.nextLabel}}>
                        <Button title={Strings.manageSubscriptions.compare}
                                onPress={ _ => {
                                                  props.select(props.keyI);
                                                  setSelected(true);
                                        }
                                }
                        />
                      </TourGuideZone>
        }else{
            return <Button title={Strings.manageSubscriptions.compare}
                           onPress={ _ => {
                                              props.select(props.keyI);
                                              setSelected(true);
                                    }
                            }
                    />
        }
    }

    return <Animated.View style={[styles.column, animatedStyles, background]}>
             <View style={styles.row}>
                <Image source={{uri: props.photo}} 
                       style={[styles.imageStyle, {marginRight: 50}]}/>
                <Text>{props.name}</Text>
                {/* <Button title="Comparar com outro..."/> */}
                </View>
                <CompareButton/>
            </Animated.View>
}

This component is already present on this second screen 2 times. At the beginning the second screen consists of 1 element of the Compared component and 2 of the Alone component. However, with the event in the Compared component, it adds two more elements alone.

I need to have a step on the button for the component Alone and another one on the one below (The ones that start on the screen, not the ones that are added), but these steps need to appear only after the event in the Compared component.

I made a logic where I can know which component is which by keyI. In my opinion it had everything to work, but I'm getting this exception:

image

Update

Taking the uses of canStart out of my context, the error started to appear in the function that unregisters the steps.

image

@ThallyssonKlein
Copy link

ThallyssonKlein commented Dec 22, 2020

I found a temporary solution:

On the second screen there are 2 steps that need to appear and two more that should only appear after some actions. Remounting the screen results in the error I showed in the message above. What I did now was:

The steps that should not appear in the sequence I put with negative keys or 0, like -2, -1, 0. And start is called on this second screen in the items ahead that need to appear, such as start(3), and 3 leads to four, which is the last recorded step.

I created a fork with a custom Tooltip for my App as a temporary solution.

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

5 participants