Skip to content

Web React


Web React Embedding

This guide explains how to embed the InvestSuite SDK in a React web application.

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 folder containing the SDK packages and a fully working example React project.

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 downloaded file inside the example folder.

In order to use this example app: 1. Navigate to the example folder 2. Run npm install or yarn install 3. Run npm start or yarn start

Install the SDK as a dependency

Add the SDK package to your project:

npm install <path-to-sdk>/investsuite-embedding-web-react

Usage of the add-to-app SDK

Import the SDK

import {
    FlutterEmbeddingContext,
    FlutterEmbeddingView,
    useFlutterEmbeddingContext,
    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-web-react';
import { ServerCallContext } from '@nicegram/grpc-web-client';

Implementing the HandoversToHostService

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

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

        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(`Opening FAQ for module: ${request.module}`);
            return Promise.resolve(StartFaqResponse.create({}));
        },

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

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

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

        startAuthorization(
            _request: StartAuthorizationRequest,
            _context: ServerCallContext
        ): Promise<StartAuthorizationResponse> {
            console.log('startAuthorization called');
            return new Promise((resolve) => {
                const result = window.confirm('Please authorize this action');
                resolve(StartAuthorizationResponse.create({ success: result }));
            });
        },

        startTransactionSigning(
            request: StartTransactionSigningRequest,
            _context: ServerCallContext
        ): Promise<StartTransactionSigningResponse> {
            console.log('startTransactionSigning:', request.portfolioId, request.amount);
            return new Promise((resolve) => {
                const result = window.confirm(`Sign transaction for ${request.amount}?`);
                resolve(StartTransactionSigningResponse.create({ success: result }));
            });
        },
    };
};

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

App Component with FlutterEmbeddingContext

Wrap your app with the FlutterEmbeddingContext provider:

import React, { useState, useCallback, useMemo } from 'react';
import {
    FlutterEmbeddingContext,
    FlutterEmbeddingView,
    useFlutterEmbeddingContext,
} from 'investsuite-embedding-web-react';

function App() {
    const [accessToken, setAccessToken] = useState('your-access-token');
    const [showFlutter, setShowFlutter] = useState(false);

    const handleExit = useCallback(() => {
        setShowFlutter(false);
    }, []);

    const handoversToHostService = useMemo(
        () => createHandoversToHostService(accessToken, handleExit),
        [accessToken, handleExit]
    );

    const startParams = useMemo(() => createStartParams(), []);

    if (!showFlutter) {
        return (
            <div className="landing-page">
                <h1>InvestSuite SDK Demo</h1>
                <input
                    type="text"
                    value={accessToken}
                    onChange={(e) => setAccessToken(e.target.value)}
                    placeholder="Enter access token"
                />
                <button onClick={() => setShowFlutter(true)}>
                    Start InvestSuite SDK
                </button>
            </div>
        );
    }

    return (
        <FlutterEmbeddingContext
            flutterAppBundlePath="flutter/main.dart.js"
            handoversToHostService={handoversToHostService}
            startParams={startParams}
        >
            <MainContent />
        </FlutterEmbeddingContext>
    );
}

MainContent Component with FlutterEmbeddingView

function MainContent() {
    const {
        loading,
        error,
        handoversToFlutterServiceClient,
    } = useFlutterEmbeddingContext();

    if (loading) {
        return <div className="loading">Loading InvestSuite SDK...</div>;
    }

    if (error) {
        return <div className="error">Error: {error.message}</div>;
    }

    return (
        <div className="app-container">
            <header className="app-header">
                <h1>InvestSuite SDK</h1>
                <CommunicationControls client={handoversToFlutterServiceClient} />
            </header>
            <main className="flutter-container">
                <FlutterEmbeddingView />
            </main>
        </div>
    );
}

CommunicationView Component

Create controls to communicate with the Flutter SDK:

interface CommunicationControlsProps {
    client: HandoversToFlutterServiceClient | null;
}

function CommunicationControls({ client }: CommunicationControlsProps) {
    const [selectedLanguage, setSelectedLanguage] = useState<Language>(Language.EN);
    const [selectedTheme, setSelectedTheme] = useState<ThemeMode>(ThemeMode.SYSTEM);
    const [deeplink, setDeeplink] = useState('/self/portfolio/DEMO/more');

    const changeLanguage = async () => {
        if (!client) return;
        try {
            const request = ChangeLanguageRequest.create({ language: selectedLanguage });
            await client.changeLanguage(request);
            console.log('Language changed successfully');
        } catch (error) {
            console.error('Error changing language:', error);
        }
    };

    const changeThemeMode = async () => {
        if (!client) return;
        try {
            const request = ChangeThemeModeRequest.create({ themeMode: selectedTheme });
            await client.changeThemeMode(request);
            console.log('Theme mode changed successfully');
        } catch (error) {
            console.error('Error changing theme mode:', error);
        }
    };

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

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

    const handleNotification = async () => {
        if (!client) 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 client.handleNotification(request);
            console.log('Notification handled successfully');
        } catch (error) {
            console.error('Error handling notification:', error);
        }
    };

    return (
        <div className="communication-controls">
            <div className="control-group">
                <label>Language:</label>
                <select
                    value={selectedLanguage}
                    onChange={(e) => setSelectedLanguage(Number(e.target.value) as Language)}
                >
                    <option value={Language.EN}>English</option>
                    <option value={Language.NL}>Dutch</option>
                    <option value={Language.FR}>French</option>
                    <option value={Language.AR}>Arabic</option>
                    <option value={Language.TR}>Turkish</option>
                </select>
                <button onClick={changeLanguage}>Change Language</button>
            </div>

            <div className="control-group">
                <label>Theme:</label>
                <select
                    value={selectedTheme}
                    onChange={(e) => setSelectedTheme(Number(e.target.value) as ThemeMode)}
                >
                    <option value={ThemeMode.LIGHT}>Light</option>
                    <option value={ThemeMode.DARK}>Dark</option>
                    <option value={ThemeMode.SYSTEM}>System</option>
                </select>
                <button onClick={changeThemeMode}>Change Theme</button>
            </div>

            <div className="control-group">
                <button onClick={() => reset(false)}>Reset (Keep Data)</button>
                <button onClick={() => reset(true)}>Reset (Clear Data)</button>
            </div>

            <div className="control-group">
                <label>Deeplink:</label>
                <input
                    type="text"
                    value={deeplink}
                    onChange={(e) => setDeeplink(e.target.value)}
                />
                <button onClick={navigateTo}>Navigate To</button>
            </div>

            <div className="control-group">
                <button onClick={handleNotification}>Send Test Notification</button>
            </div>
        </div>
    );
}

Configuration

FlutterEmbeddingContext Props

Prop Type Required Description
flutterAppBundlePath string Yes Path to the Flutter web build (main.dart.js).
handoversToHostService IHandoversToHostService Yes Your implementation of the handovers service.
startParams StartParams Yes Initial configuration for the SDK.

FlutterEmbeddingView Props

Prop Type Required Description
style CSSProperties No Custom styles for the container.
className string No Custom CSS class name.

useFlutterEmbeddingContext Hook

Returns an object with:

Property Type Description
loading boolean Whether the SDK is still loading.
error Error \| null Any error that occurred during initialization.
handoversToFlutterServiceClient HandoversToFlutterServiceClient \| null Client to communicate with Flutter.

Styling

The FlutterEmbeddingView renders an iframe that hosts the Flutter web application. Style it like any other HTML element:

.flutter-container {
    width: 100%;
    height: calc(100vh - 60px);
    border: none;
}