kt3k / lepont

🌉 A native <-> browser (webview) bridge library for react-native
MIT License
28 stars 2 forks source link

Send message to browser on React Native button click #42

Open Anthony-Gaudino opened 1 year ago

Anthony-Gaudino commented 1 year ago

It's not clear to me how one would send a message to the browser using Lepont when a user clicks for example in a button on the React Native side.

From what I can see one needs to call bridge.sendMessage but bridge is inside all this:

const [ref, onMessage] = useBridge((registry) => {
    registry.register('my-streaming-bridge', (_, bridge) => {
        bridge.sendMessage({
          type: 'my-streaming-event',
          payload: 'stream data!',
        })
    })
  })

Am I supposed to do something like this?

let stream = null;

const [ref, onMessage] = useBridge((registry) => {
    registry.register('my-streaming-bridge', (_, bridge) => {
        stream =  bridge.sendMessage;
    })
})

stream({
  type: 'my-streaming-event',
  payload: 'stream data!',
});
kt3k commented 1 year ago

Do you show the button with React Native or browser?

LePont supposes all UIs are rendered in browser (WebView) as HTML/DOM. LePont doesn't support the case where the button is rendered as React Native component.

Anthony-Gaudino commented 1 year ago

The button is shown in React Native.

After reading the Lepont source code and thinking for a while I found a solution, but maybe it could be improved. I will share the solution I have found soon. If after that you have suggestions for improvement it would be great to hear.

kt3k commented 1 year ago

I'm curious about your usage of LePont. Do you show UIs both in React Native and browser(WebView)? What part do you use RN and what part browser?

Anthony-Gaudino commented 1 year ago

We will have bidirectional communication between Native and WebView, so both can initiate or receive messages.

Currently we have buttons on native end that starts the event, but later this event would be initiated automatically on Native end. Events could also be initiated on WebView.

Anthony-Gaudino commented 1 year ago

@kt3k

In our code we are sending a stream of data from the WebView to React Native, so we first start an operation, then send the data trough multiple messages and at the end close the operation.

Here's the code structure:

React Native

import React, {useEffect} from 'react';
import {WebView} from 'react-native-webview';
import {useBridge, BridgeImpl} from 'lepont';

const MyComponent = () => {
  const [webViewRef, onMessage, {registry}] = useBridge();
  const register = (f: BridgeImpl<unknown>) => registry.register(f.name, f);
  const sendMessage = registry.sendMessage.bind(registry);

  const doSomething: BridgeImpl<any> = async (payload, bridge) => {
    // Start doing something if it was not initiated by WebView.
    if (!payload) {
      sendMessage({
        type: 'do-something',
        payload: '',
      });

      return;
    }

    // Operation finished
    const endOp = () => {};

    // Operation started
    const newOp = async () => {};

    // Does something
    const doIt = () => {};

    switch (payload.action) {
      case 'new-op':
        await newOp();
        return 'started';
      case 'do-it':
        doIt();
        return 'did-something';
      case 'end-op':
        endOp();
        return 'ended-op';
      default:
        throw 'Unknown operation!';
    }
  };

  const doSomethingElse: BridgeImpl<any> = async (payload, bridge) => {
    // Start doing something if it was not initiated by WebView.
    if (!payload) {
      sendMessage({
        type: 'do-something-else',
        payload: '',
      });

      return;
    }

    // ...
  };

  useEffect(() => {
    register(doSomething);
    register(doSomethingElse);
  }, []);

  const handleNavigationStateChange = (state: WebViewNavigation) => {};

  return (
    <View style={styles.container}>
      <WebView
        source={{uri: "index.html"}}
        originWhitelist={['*']}
        allowFileAccessFromFileURLs={true}
        allowUniversalAccessFromFileURLs={true}
        allowFileAccess={true}
        onNavigationStateChange={handleNavigationStateChange}
        onMessage={onMessage}
        ref={webViewRef}
      />
      <Button
        title="Do something"
        onPress={() => registry.registry.doSomething()}
      />
      <Button
        title="Do something else"
        onPress={() => registry.registry.doSomethingElse()}
      />
    </View>
  );
}

WebView

import {sendMessage, on} from 'lepont/browser';
import {encode} from 'base64-arraybuffer';

on('do-something', async payload => {
  /** Type of message received on Native end. */
  const msgType = 'doSomething';

  /** Payload to send to Native end. */
  const nativePayload = {
    action: '',
    data: '',
  } as const;

  // Create a new operation
  await sendMessage({
    type: msgType,
    payload: {...nativePayload, action: 'new-op'},
  });

  // Send data chunks
  await getStream(async (arrayBuffer) => {
      const answer = await sendMessage({
        type: msgType,
        payload: {
          ...nativePayload,
          action: 'do-it',
          data: encode(arrayBuffer),
        },
      });
    });

  // End operation
  await sendMessage({
    type: msgType,
    payload: {...nativePayload, action: 'end-op'},
  });
});

on('do-something-else', async payload => {
  // ...
}

There's 2 functions defined on React Native doSomething() and doSomethingElse() and they could be initiated from React Native or WebView. On the beginning of these functions I'm checking if there's a payload, if there's not then I know it was called form React Native, then it will send a message to the WebView and the WebView will send a message to Native running the function again with a payload. Of course one could also pass a payload from React Native and check in a different way.

It's important to register the functions inside a useEffect() because in the beginning when calling useBridge(), internally LePont will remove all registry entries as soon as it's called.