Invites Guide: Expo

Invites guide for the react native projects based on Expo


To enable invites for the React Native SDK, follow the Invites Guide for iOS or Android SDK to configure Branch SDK, plus the BranchIO react native SDK integration guide.

Invites Integration

An additional deep linking function of the StreamLayer SDK is the way we integrate Branch.io to invite users. Please see the documentation in the StreamLayer Studio section of this document.

  1. Create a new account on Branch.io or use your existing account.
  2. Link your Branch.io credentials with your StreamLayer account in StreamLayer Studio.
  3. Install Branch.io on the client side and add Branch.io keys into your gradle file. This is optional step - you can put keys directly to your Android Manifest, but this way is more flexible. You can find your Branch key by logging into your Branch Dashboard

Configure Expo Navigation for Branch SDK

Update +not-found.tsx file:

Modify the +not-found.tsx file by wrapping router.back() inside a setTimeout function with a 0 latency. This ensures that the back navigation is executed asynchronously, preventing any 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 an open.tsx screen:

Add a new open.tsx file within your layout structure. This file will either handle navigation to the required screen or contain all the necessary logic for the intended functionality.

New File: open.tsx

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 Setup

In 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 Setup

Initialize 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 for Branch SDK

Add to AppDelegate.swift (Swift)

  1. Import the Branch module:
import RNBranch
  1. 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];
}
  1. Implement the openURL() method:
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey: Any] = [:]) -> Bool {
  RNBranch.application(app, open:url, options:options)
  return true
}
  1. 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

In 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,
    },
});