Skip to content

Instantly share code, notes, and snippets.

@boovius
Last active July 6, 2022 23:18
Show Gist options
  • Save boovius/ffa5d4686263ae16f7db5a9acb8784a3 to your computer and use it in GitHub Desktop.
Save boovius/ffa5d4686263ae16f7db5a9acb8784a3 to your computer and use it in GitHub Desktop.
Cerca - Pendo RN integration

Intro

This gist is a selection of files that relate to the PendoRN integration in the Cerca iOS app.

Some code has been removed with comments left indicating where code has been removed.

Any code that has been removed has been deemed extraneous and irrelvant for this review.

Summary of the contents here

App.tsx

is the foundation of our app. It makes use of the custom CercaNavigationContainer.

You'll also see how we connect the app to redux and then it gets registered in the AppRegistry.

CercaNavigationContainer

is a custom component that wraps the ReactNavigation NavigationContainer. Most of the Pendo integration occurs here, including use of the withPendoRN wrapper and a function to initialize (setup) the PendoSDK.

Navigation

is a custom object that wraps up functionality in our app related to navigation.

On line 9 is where we create the ref that gets used in our NavigationContainer.

RootStackParamList

is a type used to scope the NavigationContainer props

DashboardNavigator and AuthNavigator

are 2 custom components that wrap up creation of StackNavigators

Places-list

is a component that gets consumed by the DashboardScreen which is driven by the DashboardNavigator.

On lines 40 and 59 are examples of where we are making use of the nativeID.

navigationstateobject-js

is a file that shows logged objects of navigationState going to 2 different screens in our app.

From this we can verify that we are receiving navigationState with data and we are passing that onto the onStateChange handlers from Pendo, in the NavigationContainer.

const App = ({
user, toggleUser, logout, hasSeenOnboarding, loaded, pending, dispatch, playbackFinished
}: Props) => {
// irrelvant code removed
return (
<Provider store={store}>
<CercaNavigationContainer>
{loaded ? (
<FilterProvider>
<View style={styles.container}>
{user.valid ? (<DashboardNavigator user={user} />) : (<AuthNavigator hasSeenOnboarding={hasSeenOnboarding} />)}
<PodcastLiteControls />
<Debug actions={[
{ title: Strings.debug.toggleUsers, onPress: toggleUser },
{ title: Strings.debug.clearData, onPress: logout },
]}
/>
<Modal />
<Toast />
<Spinner overlayColor={Colors.whiteWithOpacity} visible={pending} />
</View>
</FilterProvider>
) : <Spinner overlayColor={Colors.white} visible={pending} />}
</CercaNavigationContainer>
</Provider>
)
}
const mapStateToProps = (state: StoreState): StateToProps => ({
user: selectors.user.getUser(state),
hasSeenOnboarding: selectors.storage.hasSeenOnboarding(state),
loaded: selectors.storage.getLoaded(state),
pending: selectors.phone.pendingResponse(state),
})
const mapStateToDispatch = (dispatch: Dispatch): DispatchToProps => ({
dispatch,
toggleUser: () => dispatch(debugAC.toggleUser.dispatch()),
logout: () => dispatch(userAC.logOut.dispatch()),
playbackFinished: () => dispatch(phoneAC.playbackFinished.dispatch()),
})
const ConnectedApp = connect(mapStateToProps, mapStateToDispatch)(App)
const Root = () => <Provider store={store}><ConnectedApp /></Provider>
const start = (): void => {
AppRegistry.registerComponent('cerca-mobile', () => {
if (Config.STORYBOOK) {
const StorybookUIRoot = require('../storybook').default
return StorybookUIRoot
}
return CodePush(codePushOptions)(Root)
})
}
export default start
import React from 'react'
import { RootStackParamList } from '@/types'
import { Roots } from '../../constants'
import { createStackNavigator } from '../../native-modules/@react-navigation/stack'
// irrelevant code removed
const Stack = createStackNavigator<RootStackParamList>()
function AuthNavigator({ hasSeenOnboarding }: {hasSeenOnboarding: boolean}): JSX.Element {
return (
<Stack.Navigator>
{!hasSeenOnboarding ? <Stack.Screen name={Roots.Onboarding} component={OnboardingScreen} options={{ headerShown: false }} /> : null}
<Stack.Screen name={Roots.Login} component={LoginScreen} options={{ headerShown: false }} />
<Stack.Screen
name={Roots.ForgotPassword}
component={ForgotPasswordScreen}
options={{ title: 'Forgot Password', headerStyle: styles.header, ...backOptions }}
/>
<Stack.Screen
name={Roots.ResetPassword}
component={ResetPasswordScreen}
options={{ title: 'Reset Password', headerStyle: styles.header, ...backOptions }}
/>
<Stack.Screen name={Roots.SignUp} component={SignUpScreen} options={{ headerShown: false }} />
<Stack.Screen name={Roots.BasicInfo} component={BasicInfoScreen} options={{ headerShown: false }} />
</Stack.Navigator>
)
}
export default AuthNavigator
import * as React from 'react'
import { View } from 'react-native'
import { createStackNavigator } from '../../native-modules/@react-navigation/stack'
// irrelevant code removed
import { RouteProp } from '@/native-modules/@react-navigation/core'
import { RootStackParamList } from '@/types'
// irrelevant code removed
const DashboardStack = createStackNavigator<RootStackParamList>()
// irrelevant code removed
function DashboardNavigator({ user }: { user: User}): JSX.Element {
const EpisodesNav = () => <EpisodesNavigator user={user} />
return (
<DashboardStack.Navigator
screenOptions={{
headerShown: false,
headerStyle: styles.headerDashboard,
}}
>
<DashboardStack.Screen
name={Roots.Dashboard}
component={DashboardScreen}
options={({ route }) => ({
headerShown: true,
headerTitle: cercaLogo,
headerLeft: getHeaderLeft(route),
headerRight: () => (
<View style={styles.headerRightContainer}>
<Badge
onPress={() => {
navigation.navigate(Roots.ChatStack, { screen: Roots.ChatInbox })
AnalyticsTracker.chatInboxEntered()
}}
icon={Images.chatIcon}
size={Metrics.iconHeight}
borderWidth={0}
position={{ right: 44, top: 10 }}
nonCircleIcon
/>
<Badge
onPress={() => navigation.navigate(Roots.SettingsStack, { screen: Roots.Settings })}
icon={user?.profilePhotoUrl}
size={Metrics.iconHeight}
position={{ right: 10, top: 10 }}
testID={TestIDs.settings.settingsBadge}
/>
</View>
),
})}
/>
<DashboardStack.Screen
name={Roots.SettingsStack}
component={SettingsNavigator}
options={{
headerShown: false,
}}
/>
<DashboardStack.Screen
name={Roots.EpisodesStack}
component={EpisodesNav}
options={{
headerShown: false,
}}
/>
<DashboardStack.Screen
name={Roots.ChatStack}
component={ChatStack}
options={{
headerShown: true,
headerTitle: cercaLogo,
headerLeft,
}}
/>
</DashboardStack.Navigator>
)
}
export default DashboardNavigator
import React from 'react'
import { NavigationContainer, NavigationState } from '@react-navigation/native'
import navigation from '@/navigation'
import { NavigationLibraryType, PendoSDK, withPendoRN } from 'rn-pendo-sdk'
import Config from '@/config'
interface NavProps {
onStateChange: (state: NavigationState | undefined) => void
children: React.ReactElement
}
const initPendo = () => {
const navigationOptions = { library: NavigationLibraryType.Other, navigation: null }
const pendoKey = Config.PENDO_SDK_KEY
PendoSDK.setup(pendoKey, navigationOptions)
}
initPendo()
const CercaNavigationContainer = ({onStateChange, children}: NavProps) => (
<NavigationContainer
ref={navigation.ref}
onReady={() => {
navigation.mount()
const navigationState = navigation.ref.current?.getRootState()
onStateChange(navigationState)
}}
onStateChange={() => {
const navigationState = navigation.ref.current?.getRootState()
onStateChange(navigationState)
}}
>
{children}
</NavigationContainer>
)
export default withPendoRN(CercaNavigationContainer, {nativeIDs:["myProp"]})
import React, { useRef, createRef } from 'react'
import { NavigationContainerRef } from '@/native-modules/@react-navigation/native'
import { RootStackParamList } from '@/types'
const createNavigation = () => {
const isReadyRef = React.createRef<boolean>()
const navigationRef = createRef<NavigationContainerRef<RootStackParamList>>()
const ifMounted = (func: (arg?: any) => any) => (...args: any) => {
if (isReadyRef.current === true && navigationRef.current) {
func(...args)
}
}
const unmount = () => {
// @ts-ignore
isReadyRef.current = false
}
const mount = () => {
// @ts-ignore
isReadyRef.current = true
}
// @ts-ignore
const navigate = ifMounted((name: keyof RootStackParamList, params: RootStackParamList[keyof RootStackParamList]) => navigationRef.current?.navigate(name, params))
// @ts-ignore
const reset = ifMounted((index: number, routes: Array<{name: string, params: RootStackParamList[keyof RootStackParamList]}>) => navigationRef.current?.reset({ index, routes }))
const goBack = ifMounted(() => navigationRef.current?.goBack())
return {
unmount,
mount,
navigate,
reset,
goBack,
ref: navigationRef,
}
}
export default createNavigation()
## Navigation to Dashboard Screen
```
index: 0
key: "stack-gRd8hxl3wtYug7cI09M3H"
routeNames: Array(4)
0: "Dashboard"
1: "SettingsStack"
2: "EpisodesStack"
3: "ChatStack"
length: 4
__proto__: Array(0)
routes: Array(1)
0: Object
length: 1
__proto__: Array(0)
stale: false
type: "stack"
__proto__: Object
```
## Navigation to EpisodesStack Screen
```
index: 1
key: "stack-gRd8hxl3wtYug7cI09M3H"
routeNames: Array(4)
0: "Dashboard"
1: "SettingsStack"
2: "EpisodesStack"
3: "ChatStack"
length: 4
__proto__: Array(0)
routes: Array(2)
0: Object
key: "Dashboard-ACWAu9RBnlIk9zAih_Wqc"
name: "Dashboard"
params: Object
path: undefined
__proto__: Object
1: Object
key: "EpisodesStack-6rLZBhmQyGXrOBmFo-OKh"
name: "EpisodesStack"
params: Object
path: undefined
state: Object
index: 0
key: "stack-H8Fx9-hXBBsBoDo1X39LC"
routeNames: Array(4)
0: "PlaceEpisodes"
1: "PodcastPlayer"
2: "Favorites"
3: "PlaceConcierges"
length: 4
__proto__: Array(0)
routes: Array(1)
0: Object
key: "PlaceEpisodes-8kraRbi9EGcq1wHYUamw4"
name: "PlaceEpisodes"
params: Object
__proto__: Object
length: 1
__proto__: Array(0)
stale: false
type: "stack"
__proto__: Object
__proto__: Object
length: 2
__proto__: Array(0)
stale: false
type: "stack"
__proto__: Object
```
import React, { useEffect, useState } from 'react'
import { FlatList, View } from 'react-native'
import { State as PlaybackState, usePlaybackState } from 'react-native-track-player'
import { TestIDs } from '@/constants'
import { searchEpisodes } from '@/domain'
import Strings from '@/localization'
import { Episode, Place } from '@/models'
import EpisodeAlbumOptionsList from '../episode-album-list'
import SearchInput from '../fields/search'
import { H1 } from '../text'
import styles from './styles'
interface Props {
places: Array<Place>
episodes: { [key: string]: Array<Episode> }
}
const PlacesList = ({
places,
episodes,
}: Props) => {
const playbackState = usePlaybackState()
const [searchText, setSearchText] = useState('')
const [filteredEpisodes, setFilteredEpisodes] = useState<{ [key: string]: Array<Episode> }>(episodes)
useEffect(() => {
if (searchText === '' && Object.values(episodes).length > 0) {
setFilteredEpisodes(episodes)
}
}, [searchText, episodes])
const search = (txt: string) => {
const filtered = searchEpisodes(txt, places, episodes)
setFilteredEpisodes(filtered)
}
return (
<View style={styles.container} nativeID={"myProp"}>
<View style={styles.header}>
<H1 style={styles.title}>{Strings.places.dashboardHeader}</H1>
<SearchInput
placeholder={Strings.places.searchEpisodeTitle}
containerStyle={styles.searchContainer}
fieldContainerStyle={styles.searchFieldContainer}
setSearchValue={setSearchText}
searchValue={searchText}
searchAction={search}
clearSearchAction={() => {
setFilteredEpisodes(episodes)
setSearchText('')
}}
nativeID={"myProp"}
/>
</View>
<View style={styles.places} nativeID={"myProp"}>
<FlatList
testID={TestIDs.EpisodeList}
style={(playbackState === PlaybackState.Playing || playbackState === PlaybackState.Paused) && styles.list}
data={places}
keyExtractor={(item: Place) => item.uuid}
renderItem={({ item }) => {
if (filteredEpisodes[item.uuid] && filteredEpisodes[item.uuid].length > 0) {
return (
<EpisodeAlbumOptionsList
place={item}
episodes={filteredEpisodes[item.uuid]}
/>
)
}
return null
}}
/>
</View>
</View>
)
}
export default PlacesList
export type RootStackParamList = {
Login: Record<string, unknown>,
SignUp: Record<string, unknown>,
BasicInfo: Record<string, unknown>,
ForgotPassword: Record<string, unknown>,
ResetPassword: {
token: string,
tokenExpiration: string,
},
Onboarding: Record<string, unknown>,
Dashboard: Record<string, unknown>,
ChatStack: Record<string, unknown>,
ChatInbox: Record<string, unknown>,
ChatMessages: {
conversationSid: string
otherAccountId: string
},
SettingsStack: Record<string, unknown>,
Settings: Record<string, unknown>,
ProfileSettings: Record<string, unknown>,
PasswordSettings: Record<string, unknown>,
EmailSettings: Record<string, unknown>,
StorybookScreen: Record<string, unknown>,
PaymentSettings: Record<string, unknown>,
EpisodesStack: Record<string, unknown>,
PlaceEpisodes: {
place: Place,
episodes: Array<Episode>,
selectedEpisodeIndex: number,
},
PodcastPlayer: {
place: Place,
episode: Episode,
fromPlayerLite: boolean,
},
PlaceConcierges: {
place: Place,
fromPlayer?: boolean,
},
Favorites: {
place: Place,
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment