SignalR with React Native Expo:: NOTHING is happening - react-native

I have a mobile app with messaging functionality. The backend is a .NET Core 6.0 console application. The front end is a React Native Expo application. When a user hits the "send" button, it saves the message in the database and pushes the new message to the other user's screen (currently it's only one to one conversation). Everything works except when it's trying to push the new message to the recipient. I have tried other solutions on Stackoverflow but none so far has been helpful...
The .NET application is hosted on Azure and I am using the actual device to run the Expo app.
The new message is not pushed to the recipient's screen. I was getting some 404 errors before but turned out to be something else. I walked through the code and it executed the _messageHub.Clients.All.SendAsync("ReceivedMessage", message); fine but the other phone is not getting the new message.
React Native Expo code:
import { HubConnectionBuilder } from '#microsoft/signalr';
import React, { useEffect, useState } from 'react';
export default function Conversation({ navigation, route }: any) {
const [messages, setMessages] = useState([]);
const [message, setMessage] = useState("");
const conversation = route.params.data;
const personId = getCurrentUser().personId;
useEffect(() => {
loadMessages();
try {
const connection = new HubConnectionBuilder()
.withUrl(`${Settings.ApiUrl}/hubs-message`)
.build();
connection.start().then(() => console.log("started"));
connection.on("ReceivedMessage", message => {
console.log(message);
let updateMessage = messages;
updateMessage.unshift(message);
setMessages(updateMessage);
});
}
catch (e) { console.error("error here", e); }
}, []);
const sendMessage = () => {
postUser(`${Settings.ApiUrl}/v1/Message/Send`, {
message: message,
conversationId: conversation.id
}).then((data: any) => {
setMessage("");
let updateMessages = [...messages];
updateMessages.unshift(data.Message);
setMessages(updateMessages);
});
};
....
....
}
.NET Core code:
Program.cs
builder.Services.AddSignalR();
var app = builder.Build();
...
...
app.UseCors();
app.UseHttpsRedirection();
app.MapControllers();
app.UseAuthentication();
app.UseAuthorization();
app.MapHub<MessageHub>("/hubs-message");
MessageHub.cs //does not have much at the moment...
[Authorize]
public class MessageHub : Hub
{
}
NotificationService.cs //this is called by the MessageController after saving the message to the database to push the new message to the recipient.
public class NotificationService
{
private readonly IHubContext<MessageHub> _messageHub;
public NotificationService(IHubContext<MessageHub> messageHub)
{
_messageHub = messageHub;
}
...
...
if (newMessageSentData.ActiveOnConversationId == conversationId)
{
try
{
//push new message to receiver
_messageHub.Clients.All.SendAsync("ReceivedMessage", message);
}
catch (Exception ex)
{
//NO exception thrown
var a = ex.Message;
}
}
}

I was able to get it to work by moving to Azure SignalR. It's quite easy to set up. Here is a youtube video showing to to do that https://www.youtube.com/watch?v=wpQZrQ6Wnbw&t=759s.
Azure portal
Add the SignalR service to my resource group and copy the connection string under the Settings group
program.cs,
builder.Services.AddSignalR().AddAzureSignalR("[Azure SignalR Connection String]");
var app = builder.Build();
...
...
app.UseAzureSignalR(routes =>
{
routes.MapHub<MessageHub>("/hubs-message");
});

Related

React Native Firebase push notification

I have a requirement to automatically send push notifications to my application when new data is inserted into firebase.
Is there any way to do so ?
Thanks !
You can use Firebase Functions as a middleware function for sending push notifications via FCM to the device If the database value is changed.
Adding an example from my FirebaseDBtoFCMFunction repo.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.sendPushNotification = functions.database
.ref('/users/{user_id}') // Put your path here with the params.
.onWrite(async (change, context) => {
try {
const { after } = change;
const { _data } = after;
const { deviceToken } = _data.receiver; // Always send the device token within the data entry.
if(!deviceToken) return;
const payload = {
notification: {
title: 'Notification',
body: `FCM notification triggered!`
},
data: context.params // Passing the path params along with the notification to the device. [optional]
};
return await admin.messaging().sendToDevice(deviceToken, payload);
} catch (ex) {
return console.error('Error:', ex.toString());
}
});
Inside your application add child_change (valueChanged) or child_add event for specific database location than when it changes, it will fired.
From doc.
FirebaseDatabase.DefaultInstance
.GetReference("Leaders").OrderByChild("score")
.ValueChanged += HandleValueChanged;
}
void HandleValueChanged(object sender, ValueChangedEventArgs args) {
if (args.DatabaseError != null) {
Debug.LogError(args.DatabaseError.Message);
return;
}
// Do something with the data in args.Snapshot
}
For nodejs value listener

Ionic React - Navigate to a page when an FCM notification is tapped

I am implementing FCM notifications in an Ionic React application. I am having trouble navigating to another page to display the notification details.
I have created a FCMService class in my react App, and initialising this in the index.ts file.
// FCMService.ts
export default class FCMService {
public static Instance: FCMService;
private _store: Store<IAppState>;
constructor(store: Store<IAppState>) {
this._store = store;
}
public static Initalise(store: Store<IAppState>) {
if (!FCMService.Instance) {
FCMService.Instance = new FCMService(store);
FCMService.Instance.InitaliseFCM();
FCMService.Instance._store.subscribe(() => { console.log(store.getState()) });
} else {
console.debug("FCM service already intialised. Please use FCMService.Instance");
}
}
private InitaliseFCM() {
// Request permission to use push notifications
// iOS will prompt user and return if they granted permission or not
// Android will just grant without prompting
PushNotifications.requestPermission().then(result => {
console.log(result);
if (result.granted) {
// Register with Apple / Google to receive push via APNS/FCM
PushNotifications.register();
} else {
// Show some error
}
});
// On success, we should be able to receive notifications
PushNotifications.addListener('registration', (token: PushNotificationToken) => {
console.log(token);
localStorage.setItem("FCM_TOKEN", token.value);
}
);
// Some issue with our setup and push will not work
PushNotifications.addListener('registrationError',
(error: any) => {
console.log(error);
}
);
// Show us the notification payload if the app is open on our device
PushNotifications.addListener('pushNotificationReceived',
(notification: PushNotification) => {
console.log(notification);
let data = notification.notification.data as INotificationData;
}
);
// Method called when tapping on a notification
PushNotifications.addListener('pushNotificationActionPerformed',
(notification: PushNotificationActionPerformed) => {
console.log(notification);
let data = notification.notification.data as INotificationData;
this._store.dispatch(setNotificationActionCreator(data));
}
);
}
}
and then the index.ts
const store = configureStore();
interface MainProps {
store: Store<IAppState>;
}
FCMService.Initalise(store);
ReactDOM.render(<Provider store={store}><App /> </Provider>, document.getElementById('root'));
serviceWorker.unregister();
I even tried using the Redux store to save the notification on Tap - and then that would publish the notification change event (which might of worked - if I could access the useHistory() hook in the App.tsx file)
This was my attempt at navigating via Redux store in App.tsx
const App: React.FC<IProps> = ({ getCompanies, getUser, notification }) => {
console.log('app');
console.log(process.env);
const history = useHistory();
if(notification){
history.push(`/page/plot-position/{notification.id}`);
}
return (
<IonApp>
<IonReactRouter>
<IonSplitPane contentId="main" when="false">
<Menu />
<IonRouterOutlet id="main">
<Route path="/login" component={LoginPage} exact />
<PrivateRoute path="/page/plot-position/:notificationId/" component={PlotPositionPage} exact />
<Redirect from="/" to="/login" exact />
</IonRouterOutlet>
</IonSplitPane>
</IonReactRouter>
</IonApp>
);
};
const mapStateToProps = (store: IAppState) => {
return {
user: store.user.user as UserDTO,
notification: store.notificationState.notification
};
};
const mapDispatchToProps = (dispatch: any) => {
return {
getCompanies: () => dispatch(getCompaniesStartActionCreator()),
getUser: () => dispatch(getUserStartActionCreator())
}
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
It looks like your navigation works, but you're having trouble passing the notification object through to the page? You can pass the object through history state.
To access the useHistory hook you would need to make your FCMService a custom hook.
const useFCMService = (): void => {
const history = useHistory();
React.useEffect(() => {
// Method called when tapping on a notification
PushNotifications.addListener('pushNotificationActionPerformed',
(action: PushNotificationActionPerformed) => {
const notification = action.notification.data as INotificationData;
history.push({ pathname: '/page/plot-position/', state: { notification } });
}
);
}, []);
}
And then include your useFCMService custom hook in your App component.
const App: React.FC<IProps> = ({ getCompanies, getUser }) => {
useFCMService();
...
};
Deep linking provides us a way to do this: Using both an action to open the application and an action at opening the application we can enroute the user to the correct destination.
Opening the application
Here we will create an action to open the url when the user taps on the push notification; to do this less use a listener:
const {PushNotifications, App} = Plugins
***
PushNotifications.addListener(
"pushNotificationActionPerformed",
(notification: PushNotificationActionPerformed) =>{
const data = notification.notification.data;
if (data.packageNumber) App.openUrl({url: `com.company.appname://tabs/package-details/${data.packageNumber}`})
else App.openUrl({url:'/tabs'})
}
)
com.company.app:// is of capital importance since the app must reach the application must reach an existing given url, otherwise the following action(catching the url) won't be triggers since it waits a complete true from the App.openUrl function; as we are opening an internal url, this must begin with the apps given name in the capacitor config page(see the following example where we can realize how use the local url).
In this way we are adding a function to open the application in an specific route.
Redirecting the user
Here, we will complete the application's part from the deep linking tutorial: we create a new listener component who handles the appOpenUrl events and redirects to the user and we will put it on the main App file inside of its respective IonRouter:
const AppUrlListener: React.FC<any> = () => {
let history = useHistory();
useEffect(() => {
App.addListener('appUrlOpen', (data: any) => {
const slug = data.url.split(':/').pop();
if (slug) {
history.push(slug);
}
});
}, []);
return null;
};
Don't forget the route in router must begin with /, and since the application url contains :/, we split the url here and we get the second part, the slug; we push it on the history, triggering the router and getting the normal behaviour when you entering in a new route.
We will add this component inside of the router:
<IonReactRouter>
<IonSplitPane contentId="main">
<Menu />
<AppUrlListener />
<IonRouterOutlet id="main">
Now, the application will be listening the appOpenUrl event, and when it gets a new of this events, it will push the gotten url to the history, redirecting the user to that route.

react-native-background-task Expected to run on UI thread

I am trying to sync data capture offline with an online api, I periodically run an background task using react-native-background-task to retrieve offline data and sync the data with an online api.
react-native-background-task error
// This component below triggers the background task on load
import { sync, clean } from "../../services/market/forms/tasks";
import MediaWorker from "../../services/market/forms/MediaWorker";
let worker = new MediaWorker();
BackgroundTask.define(async () => {
console.log("Life's good");
// loads data from db and sync them with the online service
await sync(worker);
// delete synced data from the db and end task
await clean();
});
export default class Onboard extends Component {
constructor(props) {
super(props);
}
async checkStatus() {
const status = await BackgroundTask.statusAsync();
if (status.available) {
// schedule the background task
BackgroundTask.schedule();
return;
}
const reason = status.unavailableReason;
if (reason === BackgroundTask.UNAVAILABLE_DENIED) {
Alert.alert(
"Denied",
'Please enable background "Background App Refresh" for this app'
);
} else if (reason === BackgroundTask.UNAVAILABLE_RESTRICTED) {
Alert.alert(
"Restricted",
"Background tasks are restricted on your device"
);
}
}
componentDidMount() {
this.checkStatus();
}
render() {
// Not important for the question
}
}
// snippet for sync function
export const sync = async worker => {
const formInstances = await loadFormInstance();
if (formInstances.length) {
// Send Textual data
const formInstancesText = filterFormInstances(formInstances, "text");
postFormTextInstance(formInstancesText);
// Get form image data and post
const formInstancesImage = filterFormInstances(formInstances, "image");
formInstancesImage.forEach(worker.send);
// Get form audio data and post
const formInstancesAudio = filterFormInstances(formInstances, "audio");
formInstancesAudio.forEach(worker.send);
// Get form video data and post
const formInstancesVideo = filterFormInstances(formInstances, "video");
formInstancesVideo.forEach(worker.send);
} else {
console.log("Nothing to sync");
BackgroundTask.finish();
}
};
// snippet for clean function
export const clean = async () => {
const formInstances = await loadFormInstance();
if (formInstances.length) {
const toBeDeleted = new Set();
formInstances.forEach(formInstance => {
const fields = formInstance.fields;
let allSynced = true;
for (let index in fields) {
const field = fields[index];
if (field.synced === false) {
allSynced = false;
break;
}
}
if (allSynced) {
toBeDeleted.add(formInstance.instanceID);
}
});
toBeDeleted.forEach(deleteFormInstance);
} else {
console.log("All tasks finished");
BackgroundTask.finish();
}
};
Adb log(Used for monitoring background activity)
Note: Background task runs successfully a lot of time, but fails occasionally with the red screen shown when the app is build in debug mode.
In release mode, the app completely crashes.
Stack trace generated by Crashlytics in production
I fixed it, it turned out react-native-background-task version wasn't compatible with my react-native version, i upgraded from 0.48.1 to 0.51.0 which requires react 16.0.0

Ionic 3's PWA & Firebase Cloud Messaging registration

I was following this article here (which is not complete unfortunately) in attempt to learn how to friend Ionic 3 based PWA and Firebase Cloud Messaging: Push Notifications with FCM
What I did:
as advised in the article added FCM libraries into service-worker.js:
'use strict';
importScripts('./build/sw-toolbox.js');
importScripts('https://www.gstatic.com/firebasejs/4.9.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/4.9.0/firebase-messaging');
firebase.initializeApp({
// get this from Firebase console, Cloud messaging section
'messagingSenderId': '47286327412'
});
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler((payload) => {
console.log('Received background message ', payload);
// here you can override some options describing what's in the message;
// however, the actual content will come from the service sending messages
const notificationOptions = {
icon: '/assets/img/appicon.png'
};
return self.registration.showNotification(notificationTitle, notificationOptions);
});
self.toolbox.options.cache = {
name: 'ionic-cache'
};
// pre-cache our key assets
self.toolbox.precache(
[
'./build/main.js',
'./build/vendor.js',
'./build/main.css',
'./build/polyfills.js',
'index.html',
'manifest.json'
]
);
// dynamically cache any other local assets
self.toolbox.router.any('/*', self.toolbox.cacheFirst);
// for any other requests go to the network, cache,
// and then only use that cached resource if your user goes offline
self.toolbox.router.default = self.toolbox.networkFirst;
Then created Firebase Messaging based provider here:
import { Injectable } from "#angular/core";
import * as firebase from 'firebase';
import { Storage } from '#ionic/storage';
#Injectable()
export class FirebaseMessagingProvider {
private messaging: firebase.messaging.Messaging;
private unsubscribeOnTokenRefresh = () => {};
constructor(
private storage: Storage
) {
this.messaging = firebase.messaging();
}
public enableNotifications() {
console.log('Requesting permission...');
return this.messaging.requestPermission().then(() => {
console.log('Permission granted');
// token might change - we need to listen for changes to it and update it
this.setupOnTokenRefresh();
return this.updateToken();
});
}
public disableNotifications() {
this.unsubscribeOnTokenRefresh();
this.unsubscribeOnTokenRefresh = () => {};
return this.storage.set('fcmToken','').then();
}
private updateToken() {
return this.messaging.getToken().then((currentToken) => {
if (currentToken) {
// we've got the token from Firebase, now let's store it in the database
return this.storage.set('fcmToken', currentToken);
} else {
console.log('No Instance ID token available. Request permission to generate one.');
}
});
}
private setupOnTokenRefresh(): void {
this.unsubscribeOnTokenRefresh = this.messaging.onTokenRefresh(() => {
console.log("Token refreshed");
this.storage.set('fcmToken','').then(() => { this.updateToken(); });
});
}
}
And now during app initialization I call enableNotifications() and get error that says that default service worker is not found (404):
A bad HTTP response code (404) was received when fetching the script.
:8100/firebase-messaging-sw.js Failed to load resource: net::ERR_INVALID_RESPONSE
If I move service-worker.js firebase related stuff into default service worker in WWW folder - I get general error from Firebase (Error, failed to register service worker).
QUESTIONS:
- is there a fresh guide on Ionic 3's PWA & FCM?
- at high level what is the difference in registering service workers in Ionic 3 vs Angular? I did watch the tutorial about Angular but can't figure how to do the same in Ionic 3.
UPDATE: the below is valid as of today (02/12/2018) and most likely will be less relevant once AngularFire2 supports messaging module. So take the below with that assumption...
OK I researched and finally made it work on my Ionic 3 PWA, so I am posting solution here:
Prerequisites:
I created ionic blank app (just a home page)
installed angularfire2 and firebase ("angularfire2": "5.0.0-rc.4","firebase": "4.9.1") using npm install, I used specifically 5.0.0-rc.4" cause I had stability issues with latest one;(
created config (filename environment.ts in src folder):
export const firebaseConfig = {
apiKey: "Your Stuff Here from FB",
authDomain: "YOURAPPNAME.firebaseapp.com",
databaseURL: "https://YOURAPPNAME.firebaseio.com",
projectId: "YOURAPPNAME",
storageBucket: "YOURAPPNAME.appspot.com",
messagingSenderId: "FROMFIREBASECONEOLE"
};
I modified app.module.ts to add firebase and angularfire2 this way:
...
import { AngularFireModule } from 'angularfire2';
import 'firebase/messaging'; // only import firebase messaging or as needed;
import { firebaseConfig } from '../environment';
import { FirebaseMessagingProvider } from '../providers/firebase-messaging';
...
#NgModule({
declarations: [
MyApp,
HomePage
],
imports: [
BrowserModule,
IonicModule.forRoot(MyApp),
AngularFireModule.initializeApp(firebaseConfig),
IonicStorageModule.forRoot()
],
bootstrap: [IonicApp],
entryComponents: [
MyApp,
HomePage
],
providers: [
FirebaseMessagingProvider,
StatusBar,
SplashScreen,
{provide: ErrorHandler, useClass: IonicErrorHandler}
]
})
export class AppModule {}
Here we also import our provider whose code is below:
in providers folder I created firebase-messaging.ts like this:
import { Injectable } from "#angular/core";
import { FirebaseApp } from 'angularfire2';
// I am importing simple ionic storage (local one), in prod this should be remote storage of some sort.
import { Storage } from '#ionic/storage';
#Injectable()
export class FirebaseMessagingProvider {
private messaging;
private unsubscribeOnTokenRefresh = () => {};
constructor(
private storage: Storage,
private app: FirebaseApp
) {
this.messaging = app.messaging();
navigator.serviceWorker.register('service-worker.js').then((registration) => {
this.messaging.useServiceWorker(registration);
//this.disableNotifications()
this.enableNotifications();
});
}
public enableNotifications() {
console.log('Requesting permission...');
return this.messaging.requestPermission().then(() => {
console.log('Permission granted');
// token might change - we need to listen for changes to it and update it
this.setupOnTokenRefresh();
return this.updateToken();
});
}
public disableNotifications() {
this.unsubscribeOnTokenRefresh();
this.unsubscribeOnTokenRefresh = () => {};
return this.storage.set('fcmToken','').then();
}
private updateToken() {
return this.messaging.getToken().then((currentToken) => {
if (currentToken) {
// we've got the token from Firebase, now let's store it in the database
console.log(currentToken)
return this.storage.set('fcmToken', currentToken);
} else {
console.log('No Instance ID token available. Request permission to generate one.');
}
});
}
private setupOnTokenRefresh(): void {
this.unsubscribeOnTokenRefresh = this.messaging.onTokenRefresh(() => {
console.log("Token refreshed");
this.storage.set('fcmToken','').then(() => { this.updateToken(); });
});
}
}
Please note I init the firebase app and then in constructor we register ionic's default service worker (service-worker.js) that contains the following right after whatever is there by default:
service-worker.js:
// firebase messaging part:
importScripts('https://www.gstatic.com/firebasejs/4.9.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/4.9.0/firebase-messaging.js');
firebase.initializeApp({
// get this from Firebase console, Cloud messaging section
'messagingSenderId': 'YOURIDFROMYOURFIREBASECONSOLE'
});
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function(payload) {
console.log('Received background message ', payload);
// here you can override some options describing what's in the message;
// however, the actual content will come from the Webtask
const notificationOptions = {
icon: '/assets/images/logo-128.png'
};
return self.registration.showNotification(notificationTitle, notificationOptions);
});
At this point you also need to make sure you enabled your app as PWA, there is a good guide from Josh Morony and today there was a video stream on youtube that covers it. In TLDR you need to uncomment this in your index.html:
index.html in src uncomment:
<!-- un-comment this code to enable service worker -->
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js')
.then(() => console.log('service worker installed'))
.catch(err => console.error('Error', err));
}
</script>
OK almost the last thing - your manifest.json (in src) should have exact line:
"gcm_sender_id": "103953800507"
This concludes initial stuff on the client. Please note I didn't implement yet anything to handle notifications while user is in app itself, think for now it just handles when a message is sent from a server while your tab is not in focus (that is what I tested).
Now you want to go to your firebase console and obtain server key (click setting gear icon, then see cloud messaging section there). Copy server key. Also run the client (ionic serve and capture your local token (i just console.logged it). Now try sending yourself the message using a POST method. ( I did it with Postman)
// method: "POST",
//url: "https://fcm.googleapis.com/fcm/send",
// get the key from Firebase console
headers: { Authorization: `key=${fcmServerKey}` },
json: {
"notification": {
"title": "Message title",
"body": "Message body",
"click_action": "URL to your app?"
},
// userData is where your client stored the FCM token for the given user
// it should be read from the database
"to": userData.fcmRegistrationKey
}
So by doing all this I was able to reliable send myself a message WHILE the app was in background. I am yet to handle foreground but this SO question is about how to init default service worker and marry it with FCM.
I hope this will help some learners in future.
I have successfully implemented the process and got success response on API calls. But no notification popup coming on my browser. Any idea?
api: https://fcm.googleapis.com/fcm/send
response got:
{"multicast_id":6904414188195222649,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"0:1545375125056264%e609af1cf9fd7ecd"}]}
cheth the attached url of my console:

How to get the device token in react native

I am using react-native 0.49.3 version, My Question is how to get the device token in react native for both IOS and Android I tried with this link but it not working for me, right now I tried in IOS. how to resolve it can one tell me how to configure?
I tried different solutions and I've decided to use React Native Firebase.
Here you will find everything about Notifications.
Also, you can use the others libraries that come with Firebase, like Analytics and Crash Reporting
After set up the library you can do something like:
// utils/firebase.js
import RNFirebase from 'react-native-firebase';
const configurationOptions = {
debug: true,
promptOnMissingPlayServices: true
}
const firebase = RNFirebase.initializeApp(configurationOptions)
export default firebase
// App.js
import React, { Component } from 'react';
import { Platform, View, AsyncStorage } from 'react-native';
// I am using Device info
import DeviceInfo from 'react-native-device-info';
import firebase from './utils/firebase';
class App extends Component {
componentDidMount = () => {
var language = DeviceInfo.getDeviceLocale();
firebase.messaging().getToken().then((token) => {
this._onChangeToken(token, language)
});
firebase.messaging().onTokenRefresh((token) => {
this._onChangeToken(token, language)
});
}
_onChangeToken = (token, language) => {
var data = {
'device_token': token,
'device_type': Platform.OS,
'device_language': language
};
this._loadDeviceInfo(data).done();
}
_loadDeviceInfo = async (deviceData) => {
// load the data in 'local storage'.
// this value will be used by login and register components.
var value = JSON.stringify(deviceData);
try {
await AsyncStorage.setItem(config.DEVICE_STORAGE_KEY, value);
} catch (error) {
console.log(error);
}
};
render() {
...
}
}
Then you can call the server with the token and all the info that you need.