Skip to content

React Native


Setting up your project

There are a few requirements for a React Native project that wants to implement the InvestSuite SDK.

  • Set up the project with the React Native CLI. Expo will not give you enough control over the underlying projects to configure them correctly. If you've previously set up your project using Expo you can eject your project and continue working with the React Native CLI.
  • Make sure that you are running a Node version >= 16. If you want to be able to easily manage the Node versions on your machine we can recommend Node Version Manager (nvm).

Creating an example project

A quick example of setting up a project could look something like this. For the name of our project we will choose investsuite_react_native_embedding_example.

  1. Run your project creation command: npx react-native@0.80.0 init investsuite_react_native_embedding_example --version 0.80.0
  2. After creation, verify that your project is set up correctly by running it on both Android and iOS (simulators). If you get errors here (regarding simulator setup, for example) fix these first. Since they have nothing to do with the InvestSuite SDK but might become confusing later.
    • For iOS: cd investsuite_react_native_embedding_example && npx react-native run-ios
    • For Android: cd investsuite_react_native_embedding_example && npx react-native run-android

Download the InvestSuite SDK from your Codemagic dashboard

The InvestSuite team will provide you with a Codemagic dashboard where you can download (new) versions of the SDK as we release them. Download the latest one and store it in a convenient location where you can easily access it later.

Unzip the "artifacts" file you downloaded from Codemagic. This should give you a .tgz file that looks something like this:

The version number will of course vary based on which version you're loading.

If you unzip this file you'll not only find the SDK packages, but also a fully working example React Native project that embeds the InvestSuite SDK and gives you a nice boilerplate to the steps we explain in step 6: Usage of the add-to-app SDK.

Install the SDK as a dependency

Run the following command in your project:

npm install <path-to-tgz>/investsuite-embedding-react-native-X.X.X.tgz --save
this will adapt your package.json like this:
14c14,15
<     "react-native": "0.80.0"
---
>     "react-native": "0.80.0",
>     "investsuite-embedding-react-native": "file:investsuite-embedding-react-native-X.X.X.tgz"
29c30
< }
\ No newline at end of file
---
> }
In order to successfully run the SDK you have to do some extra changes to your iOS and Android projects. We will now do these.

Changes to your iOS project

In order to properly run the InvestSuite SDK you have to disable Flipper:

:flipper_configuration => FlipperConfiguration.disabled,
Next switch on the use_frameworks! since we need that for the SDK framework to work.

Warning

Disabling Flipper is non-optional, since it is not able to work together with Flipper. This is an issue within Flipper and React Native. The issue with Facebook (who authors and maintains React Native) has been open for 2 years, and the most recent solution they have seems to be a call to the community for help. (So we don't see this getting resolved any time soon.)

Import our podhelper.rb file to help you out with the installation of the SDK in your project.

require_relative '../node_modules/investsuite-embedding-react-native/ios-rn/Flutter/podhelper'
Then make sure that you're installing the IVSEmbeddingModule pod and all of the other dependencies we need by running the install_all_flutter_pods() from the podhelper.
pod 'IvsEmbeddingRNModule', :path => '../node_modules/investsuite-embedding-react-native'
install_all_flutter_pods()

Finally, add a post_integrate hook to deal with a CocoaPods bug and .xcfilelist files:

post_integrate do |installer|
  ivs_sdk_post_integrate('investsuite_react_native_embedding_example')
end

Complete Podfile example for React Native 0.80.0

# Resolve react_native_pods.rb with node to allow for hoisting
require Pod::Executable.execute_command('node', ['-p',
  'require.resolve(
    "react-native/scripts/react_native_pods.rb",
    {paths: [process.argv[1]]},
  )', __dir__]).strip
require_relative '../node_modules/investsuite-embedding-react-native/ios-rn/Flutter/podhelper'

platform :ios, min_ios_version_supported
prepare_react_native_project!

linkage = ENV['USE_FRAMEWORKS']
if linkage != nil
  Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
  use_frameworks! :linkage => linkage.to_sym
end

target 'investsuite_react_native_embedding_example' do
  use_frameworks!
  config = use_native_modules!

  use_react_native!(
    :path => config[:reactNativePath],
    :app_path => "#{Pod::Config.instance.installation_root}/.."
  )

  pod 'IvsEmbeddingRNModule', :path => '../node_modules/investsuite-embedding-react-native'
  install_all_flutter_pods()

  post_install do |installer|
    react_native_post_install(
      installer,
      config[:reactNativePath],
      :mac_catalyst_enabled => false,
    )
  end

  pre_install do |installer|
    installer.pod_targets.each do |pod|
      if pod.name.eql?('RNScreens')
        def pod.build_type
          Pod::BuildType.static_library
        end
      end
    end
  end

  post_integrate do |installer|
    ivs_sdk_post_integrate('investsuite_react_native_embedding_example')
  end
end

Changes to your Android project

Add the following repositories to the build.gradle file of your React Native application:

def storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"

allprojects {
    repositories {
        maven {
            url "$rootDir/../node_modules/investsuite-embedding-react-native/android-rn/Flutter/repo"
        }
        maven {
            url "$storageUrl/download.flutter.io"
        }
        jcenter()
    }
}

Set the minSdkVersion to at least 22 in build.gradle:

minSdkVersion = 22

Make sure you use a Gradle version that is > 7.3:

classpath("com.android.tools.build:gradle:7.3.0")

Also add the right distribution URL in gradle-wrapper.properties:

distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip

Because of the multiple lib/arm64-v8a/libc++_shared.so files that are available in the repositories, it is also required to add the following to your app/build.gradle:

android {
    packagingOptions {
        pickFirst '**/*.so'
    }
}

To fix possible OutOfMemory issues while building the app you can add the following to gradle.properties:

org.gradle.jvmargs=-XX\:MaxHeapSize\=1024m -Xmx1024m

Usage of the add-to-app SDK

The SDK consists of two main parts:

  • FlutterEmbeddingView: A React component that renders the Flutter application. You can use it in your own application views where you wish to render the views of the Add-to-App SDK.
  • FlutterEmbeddingModule: A service that allows you to interact with the Flutter application that is shown in the FlutterEmbeddingView.

Import the SDK

import {
    FlutterEmbeddingView,
    FlutterEmbeddingModule,
    HandoversToFlutterServiceClient,
    IHandoversToHostService,
    StartParams,
    Language,
    ThemeMode,
    // Request/Response types
    ProvideAccessTokenRequest,
    ProvideAccessTokenResponse,
    ProvideAnonymousAccessTokenRequest,
    ProvideAnonymousAccessTokenResponse,
    ReceiveAnalyticsEventRequest,
    ReceiveAnalyticsEventResponse,
    ReceiveDebugLogRequest,
    ReceiveDebugLogResponse,
    ReceiveErrorRequest,
    ReceiveErrorResponse,
    OnExitRequest,
    OnExitResponse,
    StartFaqRequest,
    StartFaqResponse,
    StartOnboardingRequest,
    StartOnboardingResponse,
    StartFundPortfolioRequest,
    StartFundPortfolioResponse,
    StartAddMoneyRequest,
    StartAddMoneyResponse,
    StartAuthorizationRequest,
    StartAuthorizationResponse,
    StartTransactionSigningRequest,
    StartTransactionSigningResponse,
    // For communicating with Flutter
    ChangeLanguageRequest,
    ChangeThemeModeRequest,
    ResetRequest,
    NavigateToRequest,
    HandleNotificationRequest,
    InvestSuiteNotificationData,
} from 'investsuite-embedding-react-native';
import { ServerCallContext } from '@protobuf-ts/runtime-rpc';

Implementing the HandoversToHostService

Before starting the engine, you need to implement the IHandoversToHostService interface which handles callbacks from Flutter to your app:

const createHandoversToHostService = (
    accessTokenRef: React.MutableRefObject<string>,
    onExit: () => void
): IHandoversToHostService => {
    return {
        provideAccessToken(
            _request: ProvideAccessTokenRequest,
            _context: ServerCallContext
        ): Promise<ProvideAccessTokenResponse> {
            console.log('provideAccessToken called');
            return Promise.resolve(
                ProvideAccessTokenResponse.create({ accessToken: accessTokenRef.current })
            );
        },

        provideAnonymousAccessToken(
            _request: ProvideAnonymousAccessTokenRequest,
            _context: ServerCallContext
        ): Promise<ProvideAnonymousAccessTokenResponse> {
            console.log('provideAnonymousAccessToken called');
            return Promise.resolve(
                ProvideAnonymousAccessTokenResponse.create({ anonymousAccessToken: '' })
            );
        },

        receiveAnalyticsEvent(
            request: ReceiveAnalyticsEventRequest,
            _context: ServerCallContext
        ): Promise<ReceiveAnalyticsEventResponse> {
            console.log('receiveAnalyticsEvent:', request.name, request.parameters);
            // Forward to your analytics service
            return Promise.resolve(ReceiveAnalyticsEventResponse.create({}));
        },

        receiveDebugLog(
            request: ReceiveDebugLogRequest,
            _context: ServerCallContext
        ): Promise<ReceiveDebugLogResponse> {
            console.log('receiveDebugLog:', request.level, request.message);
            return Promise.resolve(ReceiveDebugLogResponse.create({}));
        },

        receiveError(
            request: ReceiveErrorRequest,
            _context: ServerCallContext
        ): Promise<ReceiveErrorResponse> {
            console.error('receiveError:', request.errorCode, request.data);
            return Promise.resolve(ReceiveErrorResponse.create({}));
        },

        onExit(
            _request: OnExitRequest,
            _context: ServerCallContext
        ): Promise<OnExitResponse> {
            console.log('onExit called');
            onExit();
            return Promise.resolve(OnExitResponse.create({}));
        },

        startFaq(
            request: StartFaqRequest,
            _context: ServerCallContext
        ): Promise<StartFaqResponse> {
            console.log('startFaq:', request.module);
            Alert.alert('FAQ', `Opening FAQ for module: ${request.module}`);
            return Promise.resolve(StartFaqResponse.create({}));
        },

        startOnboarding(
            _request: StartOnboardingRequest,
            _context: ServerCallContext
        ): Promise<StartOnboardingResponse> {
            console.log('startOnboarding called');
            Alert.alert('Onboarding', 'Starting onboarding flow');
            return Promise.resolve(StartOnboardingResponse.create({ success: true }));
        },

        startFundPortfolio(
            request: StartFundPortfolioRequest,
            _context: ServerCallContext
        ): Promise<StartFundPortfolioResponse> {
            console.log('startFundPortfolio:', request.portfolioData);
            // Start your funding flow
            return Promise.resolve(StartFundPortfolioResponse.create({ success: true }));
        },

        startAddMoney(
            request: StartAddMoneyRequest,
            _context: ServerCallContext
        ): Promise<StartAddMoneyResponse> {
            console.log('startAddMoney:', request.portfolioData);
            // Start your add money flow
            return Promise.resolve(StartAddMoneyResponse.create({ success: true }));
        },

        startAuthorization(
            _request: StartAuthorizationRequest,
            _context: ServerCallContext
        ): Promise<StartAuthorizationResponse> {
            console.log('startAuthorization called');
            return new Promise((resolve) => {
                Alert.alert(
                    'Authorization',
                    'Please authorize this action',
                    [
                        {
                            text: 'Cancel',
                            onPress: () => resolve(
                                StartAuthorizationResponse.create({ success: false })
                            ),
                            style: 'cancel',
                        },
                        {
                            text: 'Authorize',
                            onPress: () => resolve(
                                StartAuthorizationResponse.create({ success: true })
                            ),
                        },
                    ],
                    { cancelable: false }
                );
            });
        },

        startTransactionSigning(
            request: StartTransactionSigningRequest,
            _context: ServerCallContext
        ): Promise<StartTransactionSigningResponse> {
            console.log('startTransactionSigning:', request.portfolioId, request.amount);
            return new Promise((resolve) => {
                Alert.alert(
                    'Transaction Signing',
                    `Sign transaction for ${request.amount}?`,
                    [
                        {
                            text: 'Cancel',
                            onPress: () => resolve(
                                StartTransactionSigningResponse.create({ success: false })
                            ),
                            style: 'cancel',
                        },
                        {
                            text: 'Sign',
                            onPress: () => resolve(
                                StartTransactionSigningResponse.create({ success: true })
                            ),
                        },
                    ],
                    { cancelable: false }
                );
            });
        },
    };
};

Creating StartParams

Create the start parameters with language, theme mode, and environment:

const createStartParams = (): StartParams => {
    return StartParams.create({
        language: Language.EN,      // EN, NL, FR, AR, TR
        themeMode: ThemeMode.SYSTEM, // LIGHT, DARK, SYSTEM
        environment: 'TST',          // "MOCK", "TST", "UAT", "PROD"
    });
};

Available Languages: - Language.EN - English - Language.NL - Dutch - Language.FR - French - Language.AR - Arabic - Language.TR - Turkish

Available Theme Modes: - ThemeMode.LIGHT - ThemeMode.DARK - ThemeMode.SYSTEM

Starting the Flutter engine

Before showing the FlutterEmbeddingView, you need to start the engine:

const [isEngineStarted, setIsEngineStarted] = React.useState(false);
const [handoversToFlutterServiceClient, setHandoversToFlutterServiceClient] = 
    React.useState<HandoversToFlutterServiceClient | null>(null);
const accessTokenRef = React.useRef<string>('your-access-token');

const startEngine = async () => {
    try {
        await FlutterEmbeddingModule.startEngine({
            startParams: createStartParams(),
            handoversToHostService: createHandoversToHostService(
                accessTokenRef,
                () => {
                    // Handle exit - navigate away from Flutter view
                    setIsEngineStarted(false);
                }
            ),
        });

        setIsEngineStarted(true);

        // Get the client for communicating with Flutter
        const client = FlutterEmbeddingModule.handoversToFlutterServiceClient();
        setHandoversToFlutterServiceClient(client);
    } catch (error) {
        console.error('Error starting engine:', error);
    }
};

FlutterEmbeddingView

Display the Flutter SDK in your React Native app:

function App() {
    const [isEngineStarted, setIsEngineStarted] = React.useState(false);

    if (isEngineStarted) {
        return (
            <FlutterEmbeddingView 
                style={{ flex: 1 }}
            />
        );
    }

    return (
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
            <Button title="Start Flutter" onPress={startEngine} />
        </View>
    );
}

Communicating with the Flutter SDK (HandoversToFlutterService)

Once the engine is started, you can call methods on the Flutter SDK using the handoversToFlutterServiceClient:

Change Language

const changeLanguage = async () => {
    if (!handoversToFlutterServiceClient) return;

    try {
        const request = ChangeLanguageRequest.create({ language: Language.FR });
        await handoversToFlutterServiceClient.changeLanguage(request);
        console.log('Language changed successfully');
    } catch (error) {
        console.error('Error changing language:', error);
    }
};

Change Theme Mode

const changeThemeMode = async () => {
    if (!handoversToFlutterServiceClient) return;

    try {
        const request = ChangeThemeModeRequest.create({ themeMode: ThemeMode.DARK });
        await handoversToFlutterServiceClient.changeThemeMode(request);
        console.log('Theme mode changed successfully');
    } catch (error) {
        console.error('Error changing theme mode:', error);
    }
};

Reset the SDK

const reset = async (clearData: boolean) => {
    if (!handoversToFlutterServiceClient) return;

    try {
        const request = ResetRequest.create({ clearData });
        await handoversToFlutterServiceClient.reset(request);
        console.log(`Reset called with clearData=${clearData}`);
    } catch (error) {
        console.error('Error resetting:', error);
    }
};
const navigateTo = async (deeplink: string) => {
    if (!handoversToFlutterServiceClient) return;

    try {
        const request = NavigateToRequest.create({ deeplink });
        await handoversToFlutterServiceClient.navigateTo(request);
        console.log(`NavigateTo called with deeplink=${deeplink}`);
    } catch (error) {
        console.error('Error navigating:', error);
    }
};

// Example usage:
navigateTo('/self/portfolio/DEMO/more');

Handle a notification

const handleNotification = async () => {
    if (!handoversToFlutterServiceClient) return;

    try {
        const notificationData = InvestSuiteNotificationData.create({
            id: 'notification-123',
            title: 'Deposit Received',
            body: 'Your deposit has been processed',
            type: 'CASH_DEPOSIT_EXECUTED',
            module: 'SELF',
            createdAt: BigInt(Date.now()),
            data: { portfolio_id: 'DEMO' },
        });

        const request = HandleNotificationRequest.create({
            notificationData,
        });

        await handoversToFlutterServiceClient.handleNotification(request);
        console.log('Notification handled successfully');
    } catch (error) {
        console.error('Error handling notification:', error);
    }
};

Stop the Flutter engine

After using the SDK, you can stop the engine to free up resources:

FlutterEmbeddingModule.stopEngine();
setIsEngineStarted(false);
setHandoversToFlutterServiceClient(null);

Example app

Each SDK embedding module ships with a functional example app that shows how to use the SDK. You can find this by unzipping the .tgz file you downloaded from Codemagic inside the package > example folder.

In order to use this app you should cd into the package folder, make sure that you're running the correct Node version (see setting up your project) and run yarn run bootstrap (or npm run bootstrap if you're using npm).

This will install all the dependencies from npm for both the SDK and the example project, and make sure all the podfiles are downloaded and installed.

After this you can run the example app by cd-ing into the example directory and running yarn run ios or yarn run android (or npm run ios / npm run android if you're using npm).