Invites Guide: Expo
Configure Branch.io deep linking for StreamLayer invites in your Expo project. Covers native setup for Android and iOS, Expo navigation routing for incoming links, and the Branch event listener.
Invites Guide: Expo
This guide covers how to enable invite deep linking in Expo-based React Native projects using Branch.io.
To enable invites for the React Native SDK, follow the Invites Guide for iOS or Android to configure the Branch SDK on each native platform, and refer to the Branch.io React Native integration guide for general setup.
Invites Integration
StreamLayer integrates Branch.io for invite deep linking. For a high-level overview of how invites work, see the Inviting Additional Users section in the StreamLayer Studio documentation.
- Create a new account on Branch.io or use your existing account.
- Link your Branch.io credentials with your StreamLayer account in StreamLayer Studio.
- Install Branch.io on the client side and add the Branch.io keys to your project configuration. You can find your Branch key in your Branch Dashboard.
Configure Expo Navigation for Branch SDK
Update +not-found.tsx
+not-found.tsxModify the +not-found.tsx file by wrapping router.back() inside a setTimeout with zero delay. This ensures back navigation executes asynchronously, preventing potential issues with immediate execution:
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { useRouter } from 'expo-router'
export default function NotFoundScreen() {
const router = useRouter()
setTimeout(() => {
router.back();
}, 0);
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<ThemedView style={styles.container}>
<ThemedText type="title">This screen doesn't exist.</ThemedText>
<Link href="/" style={styles.link}>
<ThemedText type="link">Go to home screen!</ThemedText>
</Link>
</ThemedView>
</>
);
}
Create open.tsx
open.tsxAdd a new open.tsx file within your layout structure. This file handles navigation to the required screen or contains the logic for processing incoming deep links:
import { useRouter } from 'expo-router';
import { View, Text, StyleSheet } from 'react-native';
export default function OpenScreen(): React.JSX.Element {
// Any needed navigation
const router = useRouter();
return (
<View style={styles.absoluteFillObject}>
<Text>Open.tsx Screen</Text>
</View>
);
}
const styles = StyleSheet.create({
absoluteFillObject: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
Install Branch SDK
Android Setup
MainActivity.kt
MainActivity.ktIn the onCreate() method, force a new Branch session on every new intent:
import io.branch.rnbranch.*
class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme)
// Force a new Branch session on every new intent
val intent = intent
intent.putExtra("branch_force_new_session", true)
setIntent(intent)
super.onCreate(savedInstanceState)
}
override fun onStart() {
super.onStart()
// Initialize the Branch session with intent data
RNBranchModule.initSession(intent.data, this)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
// Reinitialize the Branch session when a new intent is received
RNBranchModule.reInitSession(this)
// Optional: Handle deep linking with other modules if required (e.g., StreamLayer)
StreamLayer.handleDeepLink(intent, this)
}
}MainApplication.kt
MainApplication.ktInitialize the Branch SDK in the onCreate() method:
import io.branch.rnbranch.*
class MainApplication : Application(), ReactApplication {
override fun onCreate() {
super.onCreate()
// Initialize Branch SDK
RNBranchModule.getAutoInstance(this)
// Enable Branch SDK logging for debugging
RNBranchModule.enableLogging()
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
}iOS Setup
AppDelegate.swift
AppDelegate.swift- Import the Branch module:
import RNBranch- Initialize Branch in
didFinishLaunchingWithOptions:
Swift:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
RNBranch.initSession(launchOptions: launchOptions)
return true
}Objective-C:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.moduleName = @"example";
// You can add your custom initial props in the dictionary below.
// They will be passed down to the ViewController used by React Native.
self.initialProps = @{};
[RNBranch initSessionWithLaunchOptions:launchOptions isReferrable:YES];
self.sdk = [[SLRObjCWrapper alloc] init];
[self.sdk setupSDK];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- Implement the
openURL()method:
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey: Any] = [:]) -> Bool {
RNBranch.application(app, open:url, options:options)
return true
}- Implement the
continueUserActivity()method:
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
RNBranch.continue(userActivity)
return true
}React Native Configuration for Branch
Create branch-config.js
branch-config.jsIn the root of your project, create a file named branch-config.js with the following content:
export default {
apiKey: 'key_live_hnN8KTW5btUZkBFhsP8QIhbdExmquc8K',
};Add Branch Event Listener
import branch from 'react-native-branch';
branch.subscribe(({ error, params, uri }) => {
if (error) {
console.error('Error from Branch: ' + error);
return;
}
if (params['+clicked_branch_link']) {
if (Platform.OS === 'ios') {
StreamLayer.getInvite({ streamlayer: params.streamlayer });
} else if (Platform.OS === 'android') {
const sendInvite = async () => {
const invite = await StreamLayer.getInvite(params.streamlayer);
setTimeout(() => {
if (params.streamlayer !== undefined) {
viewRef.current?.handleInvite(params.streamlayer);
}
}, 1000);
};
sendInvite();
}
}
});Next, see an example of handling universal links and routing to the exact tab with StreamLayer invite handling:
import React, { useEffect } from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createNavigationContainerRef } from '@react-navigation/native';
import FeedScreen from './screens/FeedScreen';
import LiveScreen from './screens/LiveScreen';
import PlayerScreen from './screens/PlayerScreen';
import ProfileScreen from './screens/ProfileScreen';
import SettingsScreen from './screens/SettingsScreen';
import branch, { BranchParams } from 'react-native-branch';
import { StreamLayer } from 'react-native-streamlayer';
class LBarState {
slideX: number;
slideY: number;
constructor(slideX: number, slideY: number) {
this.slideX = slideX
this.slideY = slideY
}
}
export default function open(): React.JSX.Element {
const [volumeBeforeDucking, setVolumeBeforeDucking] = useState<number | undefined>(undefined)
const [isPortrait, setPortrait] = useState<boolean>();
const [player, setPlayer] = useState<THEOplayer | undefined>(undefined);
const [lbarState, setLbarState] = useState(new LBarState(0, 0));
const [events, setEvents] = useState<Array<StreamLayerDemoEvent>>()
const [currentEventId, setCurrentEventId] = useState<String>()
const [isInitialized, setInitialized] = useState(false);
const viewRef = useRef<StreamLayerView>(null);
useEffect(() => {
setPortrait(isScreenPortrait())
const initialize = async () => {
await ScreenOrientation.unlockAsync();
try {
checkInitialized();
} catch (error) {
console.error("Error initializing:", error);
}
};
initialize();
branch.subscribe(
({ error, params, uri }) => {
if (error) {
console.error('Error from Branch: ' + error);
return;
}
if (params['+clicked_branch_link']) {
console.log('Branch params:', params.streamlayer);
if(Platform.OS === 'ios') {
StreamLayer.handleDeepLink({streamlayer: params.streamlayer})
} else if (Platform.OS === 'android') {
processBranchLink(params)
}
}
}
);
}, []);
return (
<SafeAreaView style={{...styles.container, marginTop: insets.top }} edges={['top']}>
{(isPortrait) &&
<View style={{ flex: 1, marginTop: playerHeight - insets.top }}>
{currentEvent !== undefined && (
<Text style={styles.eventTitle}>{currentEvent.title}</Text>
)}
<ScrollView style={{ flex: 1 }}>
{scrollItems}
</ScrollView>
</View>
}
{isInitialized &&
<StreamLayerView
style={StyleSheet.absoluteFillObject}
ref={viewRef}
config={viewConfig}
applyWindowInsets={false}
onRequestStream={onRequestStream}
onLBarStateChanged={onLBarStateChanged}
onRequestAudioDucking={onRequestAudioDucking}
onDisableAudioDucking={onDisableAudioDucking}
player={streamLayerViewPlayer}
playerView={
<THEOplayerView config={playerConfig} onPlayerReady={onReady}
style={{
width: Dimensions.get('screen').width - lbarState.slideX,
height: playerHeight-lbarState.slideY,
paddingTop: 0,
}}>
{player !== undefined && (
<UiContainer
theme={DEFAULT_THEOPLAYER_THEME}
player={player}
center={
<CenteredControlBar
left={<SkipButton skip={-10} />}
middle={<PlayButton />}
right={<SkipButton skip={10} />}
/>
}
/>
)}
</THEOplayerView>
}
/>
}
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'gray',
},
eventTitle: {
color: 'white',
fontSize: 24,
margin: 4
},
eventRowTitle: {
flex: 1,
color: 'white',
fontSize: 16,
margin: 4
},
eventRow: {
flexDirection: 'row',
margin: 4,
padding: 4,
height: 58,
justifyContent: 'flex-start',
alignItems: 'center',
borderWidth: 1,
borderColor: 'black',
},
eventRowImage: {
width: 100,
height: 50
},
overlay: {
paddingTop: 500,
backgroundColor: '#00000000',
flex: 1,
},
});Related
- Invites Guide: React Native — Branch.io setup for bare React Native projects
- Invites Guide (iOS) — Native iOS Branch.io configuration
- Invites Guide (Android) — Native Android Branch.io configuration
- Inviting Additional Users — StreamLayer Studio invite configuration
Updated 21 days ago
