Rails turbolinks server with react-native-webview app and firebase messaging

I have the following setup:

A rails server, which has a basic user login using the devise gem. There I’ve defined a User and a Device model where each user may have multiple devices via a has_many relationship on the user. The Device model has a single attribute- a token which I would use to send push notifications to using the rpush gem.

A react-native app using the react-native-webview package and the react-native-firebase/messaging package. The thing is, that I don’t really know how to send the firebase device token to the server correctly.

What I’ve got so far is that I’ve overridden the rails devise registrations#new view to the following:

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <%= f.fields_for :devices, @user do |f| %>
    <div class="field">
      <%= f.hidden_field :token, {value: nil} %>
    </div>
  <% end %>

  <div class="actions">
    <%= f.submit "Sign up" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

Here you can see that I’ve added a hidden field for the token, which I insert in the react-native app once I’ve landed on the /users/sign_up page using the following code:

import React, { useState } from "react";
import { WebView } from "react-native-webview";
import messaging from '@react-native-firebase/messaging';

const MyWebView = () => {
  const [uri] = useState("http:\/\/10.0.2.2:3000");
  const [webViewRef, setWebViewRef] = useState(null);

  const postMessageThatTurbolinksLoadOccured = `
    document.addEventListener("turbolinks:load", () => {
      window.ReactNativeWebView.postMessage("turbolinks:load");
    })`;

  function onMessage(obj) {
    const event = obj.nativeEvent;

    if(event.data != "turbolinks:load") return;

    if(event.url==`${uri}/users/sign_in` || event.url==`${uri}/users/sign_up`) {
      messaging().getToken().then(token => {
        const insertTokenCode = `
          document.getElementById("user_devices_attributes_0_token").value = "${token}";
        `;
        webViewRef.injectJavaScript(insertTokenCode);
      })
    }
  }

  // This is here only temporarily
  messaging().onMessage(remoteMessage => {
    Alert.alert(remoteMessage.notification.body);
  })

  return (
    <WebView
      ref={setWebViewRef}
      source={{ uri: `${uri}/users/sign_in` }}
      injectedJavaScript={postMessageThatTurbolinksLoadOccured}
      onMessage={onMessage}
    />
    );
};

export default MyWebView;

And this actually works fine, but once I got around to testing the rails app and I tried setting the hidden field value in the specs I got an error:

     Selenium::WebDriver::Error::ElementNotInteractableError:
       element not interactable

Which made me think that this approach is “hacky”. So does anybody know a better approach?

To anyone who encounters this problem as well,I figured it out a while ago, but haven’t gotten around to answering the question.

I started using firebase channels instead of having to register each device on the server and just send the message to the channel instead, which removes the need for me to save each device ID on the server. Like so:

  • For the react-native app
import { Alert } from "react-native";
import { WebView } from "react-native-webview";
import messaging from '@react-native-firebase/messaging';

const MyView = () => {
  const messagingInstance = messaging()
  messagingInstance.subscribeToTopic("welcome").then(() => {
    messagingInstance.onMessage(message => {
      if(message.from == "/topics/welcome") {
        Alert.alert(message.notification.body);
        messagingInstance.unsubscribeFromTopic("welcome");
      }
    });
  });

  return (
    <WebView
      source={{ uri: `http:\/\/10.0.2.2:3000` }}
    />
    );
};
export default MyView;
  • on the server I have a job which sends a notification via the channel using the fcm gem
class SendGreetingPushNotificationJob < ApplicationJob
  queue_as :default

  def perform(*args)
    fcm = FCM.new(Rails.application.config.FIREBASE_SERVER_KEY)
    fcm.send_to_topic("welcome", notification: {body: "Welcome to MyApp!"})
  end
end