Code Monkey home page Code Monkey logo

Comments (11)

Voltron369 avatar Voltron369 commented on May 23, 2024 6

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.

from rn-tourguide.

patrickspafford avatar patrickspafford commented on May 23, 2024 5

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.

from rn-tourguide.

Arun-paramasivam avatar Arun-paramasivam commented on May 23, 2024 1

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') } } })

from rn-tourguide.

ThallyssonKlein avatar ThallyssonKlein commented on May 23, 2024

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

from rn-tourguide.

patrickspafford avatar patrickspafford commented on May 23, 2024

@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?

from rn-tourguide.

ThallyssonKlein avatar ThallyssonKlein commented on May 23, 2024

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

from rn-tourguide.

ThallyssonKlein avatar ThallyssonKlein commented on May 23, 2024

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.

from rn-tourguide.

Voltron369 avatar Voltron369 commented on May 23, 2024

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

from rn-tourguide.

ThallyssonKlein avatar ThallyssonKlein commented on May 23, 2024

It worked perfectly, thanks.

@Voltron369

from rn-tourguide.

ThallyssonKlein avatar ThallyssonKlein commented on May 23, 2024

@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

from rn-tourguide.

ThallyssonKlein avatar ThallyssonKlein commented on May 23, 2024

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.

from rn-tourguide.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.