Detected country: US
logo
Open AppStatusSubmit Ticket
GuideAPI References
‌
‌
‌
logo

Powered by

  • Home
  • Advanced
  • Actions
  • How to implement Client Actions in a React Native app (via WebView) [ALPHA]

How to implement Client Actions in a React Native app (via WebView) [ALPHA]

9min read

Share

Client actions allow your React Native app to communicate with the Brainfish widget when it's embedded in a WebView. This guide shows you two clear implementation options.

Prerequisites

  • React Native app with react-native-webview installed
  • Brainfish Agent widget rendered inside the WebView
  • A Client Action created in Brainfish (with the Action Key copied)

Two Implementation Options

Option 1: Pure Web Implementation

Run the action logic entirely within the WebView (e.g., calling APIs with fetch).

Option 2: Native-Bridged Implementation (Recommended)

Forward actions to React Native for native capabilities, then return results to the WebView.


Option 1: Pure Web Implementation

Handle everything directly in your web page without native code interaction.

Step 1: Set Up Your Client Action

Follow this guide to configure your action in Brainfish: Setting up a Client Action

Step 2: Basic WebView Setup

import React from 'react';
import { SafeAreaView } from 'react-native';
import { WebView } from 'react-native-webview';

export default function App() {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <WebView 
        source={{ uri: 'https://your-website.com' }}
        javaScriptEnabled={true}
        style={{ flex: 1 }}
      />
    </SafeAreaView>
  );
}

Step 3: Register Handler in Your Web Page

Add this to your web page where the Brainfish widget loads:

// Pure web implementation - everything happens in the browser
Brainfish.Widgets.registerClientActionHandler('bf_action_abc123', async (inputs) => {
  // Get the validated inputs
  const { email, phone, username } = inputs;
  
  // Call your API directly from the web page
  const response = await fetch('https://api.yourapp.com/me', {
    method: 'POST',
    headers: { 
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + window.authToken 
    },
    body: JSON.stringify({ email, phone, username })
  });
  
  if (!response.ok) {
    throw new Error('Failed to update account');
  }
  
  const { updatedAt } = await response.json();
  return { updatedAt }; // Must return an object
});

Option 2: Native-Bridged Implementation (Recommended)

Use this when you need native capabilities like device SDKs, secure storage, or native modules.

Step 1: React Native Side - Set Up Bridge

import React, { useRef, useCallback } from 'react';
import { SafeAreaView } from 'react-native';
import WebView from 'react-native-webview';

export default function AppWebView() {
  const webRef = useRef(null);

  // Send success response back to web
  const resolveToWeb = useCallback((id, payload) => {
    const js = `
      window.__bfResolveAction && window.__bfResolveAction(${JSON.stringify({
        id,
        ok: true,
        payload,
      })});
      true;
    `;
    webRef.current?.injectJavaScript(js);
  }, []);

  // Send error response back to web
  const rejectToWeb = useCallback((id, error) => {
    const js = `
      window.__bfResolveAction && window.__bfResolveAction(${JSON.stringify({
        id,
        ok: false,
        error,
      })});
      true;
    `;
    webRef.current?.injectJavaScript(js);
  }, []);

  // Handle messages from WebView
  const handleMessage = useCallback(async (event) => {
    try {
      const msg = JSON.parse(event.nativeEvent.data);
      if (msg?.type !== 'brainfish:action') return;

      const { id, actionKey, inputs } = msg;

      // Route actions by their key
      switch (actionKey) {
        case 'bf_action_abc123':
          try {
            // Your native logic here
            const result = await updateUserProfile(inputs);
            resolveToWeb(id, { updatedAt: result.updatedAt });
          } catch (err) {
            rejectToWeb(id, err.message);
          }
          break;

        default:
          rejectToWeb(id, `Unknown action: ${actionKey}`);
      }
    } catch (err) {
      console.warn('Bridge error:', err);
    }
  }, [resolveToWeb, rejectToWeb]);

  // Inject bridge setup code
  const injectedJavaScript = `
    (function(){
      const pending = new Map();

      // Generate unique ID
      function uuid(){ 
        return Math.random().toString(36).slice(2) + Date.now(); 
      }

      // React Native calls this to resolve/reject
      window.__bfResolveAction = ({ id, ok, payload, error }) => {
        const p = pending.get(id);
        if (!p) return;
        pending.delete(id);
        ok ? p.resolve(payload) : p.reject(new Error(error || 'Action failed'));
      };

      // Create a handler that forwards to React Native
      window.__bfMakeNativeBackedHandler = function(actionKey){
        return function(inputs){
          const id = uuid();
          
          // Send to React Native
          window.ReactNativeWebView?.postMessage(JSON.stringify({
            type: 'brainfish:action',
            id,
            actionKey,
            inputs
          }));
          
          // Wait for response (with 5s timeout)
          return new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
              pending.delete(id);
              reject(new Error('Action timed out'));
            }, 4900);
            
            pending.set(id, {
              resolve: (v) => { 
                clearTimeout(timeout); 
                resolve(v || {}); 
              },
              reject: (e) => { 
                clearTimeout(timeout); 
                reject(e); 
              }
            });
          });
        };
      };
    })();
    true;
  `;

  return (
    <SafeAreaView style={{ flex: 1 }}>
      <WebView
        ref={webRef}
        source={{ uri: 'https://your-website.com' }}
        onMessage={handleMessage}
        injectedJavaScript={injectedJavaScript}
        javaScriptEnabled={true}
        originWhitelist={['https://*']}
        style={{ flex: 1 }}
      />
    </SafeAreaView>
  );
}

// Example native function
async function updateUserProfile({ email, phone, username }) {
  // Call native modules, secure APIs, etc.
  // Must complete within ~4.5 seconds
  const response = await fetch('https://api.yourapp.com/me', {
    method: 'POST',
    headers: { 
      'Authorization': await getSecureToken() // Native secure storage
    },
    body: JSON.stringify({ email, phone, username })
  });
  
  return { updatedAt: new Date().toISOString() };
}

Step 2: Register Handler in Your Web Page

Add this to your web page where Brainfish loads:

// Register the native-backed handler
Brainfish.Widgets.registerClientActionHandler(
  'bf_action_abc123',
  window.__bfMakeNativeBackedHandler('bf_action_abc123')
);

// Register multiple actions if needed
Brainfish.Widgets.registerClientActionHandler(
  'bf_action_xyz',
  window.__bfMakeNativeBackedHandler('bf_action_xyz')
);

Setting Up Your Client Action in Brainfish

Step 1: Define Your Action

  1. Go to Actions → Create new action → Client Action
  2. Enter an Action name (e.g., "Update profile")
  3. Add optional Description with trigger examples
  4. Click Configure your inputs

Step 2: Configure Inputs

Add the inputs your action needs:

  • Input Name: Internal variable name (e.g., email)
  • Required: Toggle if this input is mandatory
  • Input Type: Choose from Short Answer, Email, Phone, etc.
  • Label: User-friendly label for the form

Step 3: Apply Filters

  • Show on: Start with a test Agent only
  • Click Save & Publish

Step 4: Copy Your Action Key

Click the context menu → Copy Action Key (e.g., bf_action_abc123)


Quick Decision Guide

Choose Option 1 (Pure Web) if:

  • You only need to call web APIs
  • No native features required
  • Simpler implementation preferred

Choose Option 2 (Native-Bridged) if:

  • You need native device capabilities
  • Secure token storage required
  • Complex native business logic
  • Better error handling needed

Important Notes

⏱️ Timing Requirements

  • Actions must complete within 5 seconds
  • Set timeout to ~4.9 seconds to be safe
  • Keep native operations fast

📦 Return Values

  • Always return an object (even { ok: true })
  • Throw an Error to signal failure: throw new Error('Reason')
  • Include enough data for the AI agent to understand the outcome

🔒 Security Best Practices

  • Don't pass secrets through WebView messages
  • Use originWhitelist to limit allowed domains
  • Validate all inputs on the native side
  • Store auth tokens securely in native code

Testing Your Implementation

  1. Set up a test Agent in Brainfish
  2. Create test action with "Show on" filter for test Agent only
  3. Test in staging environment first
  4. Verify scenarios:
    • Success cases
    • Network failures
    • Validation errors
    • Timeout handling
  5. Remove filters when ready for production

Troubleshooting

Action not triggering

  • Verify Action Key matches exactly
  • Check handler is registered after Brainfish loads
  • Ensure javaScriptEnabled={true} on WebView

Messages not received

  • Check window.ReactNativeWebView exists
  • Verify message type is 'brainfish:action'
  • Add console.log debugging

Timeout errors

  • Reduce processing time to under 4.5 seconds
  • Run parallel operations where possible
  • Return minimal data needed

Need Help?

  • Check the Brainfish Setup Guide
  • Review react-native-webview documentation
  • Contact Brainfish support for assistance

Share