Is there a way to translate / localise notifications sent from Firebase?
I have my app setup to receive notifications successfully:
extension AppDelegate: UNUserNotificationCenterDelegate {
func setupNotifications() {
registerForRemoteNotifications()
setupNotificationTokenRefresh()
}
func registerForRemoteNotifications() {
let application = UIApplication.shared
UNUserNotificationCenter.current().delegate = self
application.registerForRemoteNotifications()
}
func setupNotificationTokenRefresh() {
storeNotificationToken()
_ = NotificationCenter.default.addObserver(
forName: .MessagingRegistrationTokenRefreshed,
object: nil,
queue: .current
) { [weak self] _ in
self?.storeNotificationToken()
}
}
private func storeNotificationToken() {
Messaging.messaging().token { [weak self] token, error in
if let error = error {
Log.error("Error fetching FCM registration token: \(error)")
} else if let token = token {
// save token
}
}
}
}
A payload is sent from a Firebase cloud function with a data object and I would like to access this data object and translate/localize the message sent.
I looked into several methods provided, but they seem to be about intercepting notifications only when the app is in the foreground, which is not what I am interested in.
The payload sent from the server:
const payload = {
notification: {
title: 'Friend request',
body: senderName + ' wants to add you as a friend'
},
data: {
senderUserId: friendRequestFrom,
type: 'friendRequest'
}
}
Because you are already using cloud functions, one way to accomplish that is to do the translation server-side with the Google Cloud Translation API. There is a good sample demonstrating how to do so with Node.js.
Say for example you are sending the notifications when new objects get added to the /notifications path in your real-time database. You can do something like this:
const Translate = require('#google-cloud/translate')
const functions = require('firebase-functions')
const projectId = 'YOUR_PROJECT_ID'
const translate = new Translate({
projectId: projectId,
});
const admin = require('firebase-admin')
admin.initializeApp(functions.config().firebase)
exports.sendNotification = functions.database.ref(`/notifications/{notif_id}`)
.onWrite(event => {
const notification = event.data.val()
// Don't send when this isn't a new notification.
if (event.data.previous.exists()) {
return null
}
const user_id = notification.user_id
getLocalLanguageOfUser(user_id).then(language => {
if (language != 'en')
translateTo(language, notification).then(localised => {
return send(localised)
}).catch(err => {
return console.log(err)
})
} else { // it's English - no need to translate
return send(notification)
}
})
})
function getLocalLanguageOfUser(user_id) {
return new Promise((resolve, reject) => {
// default will be 'en' for English
firebase.database().ref(`users/${user_id}/language`)
.once('value').then(snapshot => {
resolve(snapshot.val() || 'en')
})
.catch(err => reject(err))
})
}
function translateTo(language, notification) {
return new Promise((resolve, reject) => {
const text = notification.text;
translate.translate(text, language).then(results => {
const translation = results[0];
resolve({
...notification,
text: translation
})
}).catch(err => reject(err))
})
}
function send(notification) {
// use your current setup to send the notification.
// 'text' key will already be localised.
}
Instead of body in your payload use body_loc_key and put the string in your application resourceId.
In onMessageRecieved:
String theKeyFromPayload= remotemessage.getNotification.getBodyLocalizationKey()
String resourceAppStatusString=theKeyFromPayload
Int resourceId= getResourceId(resourceAppStatusString, "string", this.getPackageName()
String finalBody = getResource(). getResourceName(resourceId);
finalBody is passed into the notification in your application.
Related
I've created an Ionic chat app with firebase cloud functions. The push notifications are working with Android but not ios.
async getIosToken(token: string, userId: string): Promise<void> {
if (!FCM.hasPermission()) {
FCM.requestPushPermission()
.then(async (hasPerm) => {
if (hasPerm) {
const iosToken = await FCM.getAPNSToken();
if (iosToken === token) {
return;
} else {
this.saveToken(iosToken, userId);
}
}
});
} else {
const iosToken = await FCM.getAPNSToken();
if (iosToken === token) {
return;
} else {
this.saveToken(iosToken, userId);
}
}
}
saveToken(token: string, userId: string): void {
this.userSvc.saveTokenToFirestore(token, userId)
.then(() => {
this.storageSvc.setDeviceToken(token);
});
}
The iOS token is being saved to firebase...although it never prompted the user for request permissions.
I console logged the firebase cloud function and I can see the APNs token.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
exports.newChatNotification = functions.firestore
.document(`chat/{id}/messages/{doc}`)
.onWrite(async event => {
const message = event.after.data();
let data: any;
let device: any;
const db = admin.firestore();
console.log('message', message);
console.log('db', db);
if (message) { data = message; }
const receivingUserId = data ? data.receivingUserId : '';
const content = data ? data.content : '';
const sendingUserId = data ? data.sendingUserId : '';
console.log('payload', receivingUserId, sendingUserId);
const payload = {
notification: {
title: 'New message',
body: `${content}`,
click_action: 'FCM_PLUGIN_ACTIVITY'
},
data: {
page: 'tabs/travel-buddies',
}
};
console.log('payload2', payload);
const devicesRef = (await db.collection('devices').doc(`${receivingUserId}`).get()).data();
if (devicesRef) { device = devicesRef; }
const token = device ? device.token : '';
console.log('devicesRef', devicesRef, token);
return admin.messaging().sendToDevice(token, payload);
});
Here's the firebase cloud function console
I'm not sure how to troubleshoot why ios is not receiving a push notification because there aren't any errors and I'm getting the APNs token.
I've also tried updating the build system per Google searches online:
Any help would be greatly appreciated.
The cancellation response from token.botframework.com is currently being displayed to screen like this:
{
"error": {
"code": "ServiceError",
"message": "Missing required query string parameter: code. Url = https://token.botframework.com/.auth/web/redirect?state=d48fb60ae4834fd8adabfe054a5eff74&error_description=The+user+chose+not+to+give+your+app+access+to+their+Dropbox+account.&error=access_denied"
}
}
How can I, instead, handle the cancellation gracefully? If the user cancels like this, I'd like to just have the auth-card-popup window close automatically.
This is for an action-type messaging extension app that I'm building. The sign-in process begins with an auth card. The bot is pointed at a Dropbox OAUTH2 connection. Here the relevant code that brings up the card:
const { TeamsActivityHandler, CardFactory } = require('botbuilder');
class MsgExtActionBot extends TeamsActivityHandler {
constructor() {
super();
this.connectionName = 'oauth2-provider';
}
async handleTeamsMessagingExtensionFetchTask(context, action) {
if (!await this.isAuthenticated(context)) {
return this.getSignInResponse(context);
}
}
async isAuthenticated(context) {
let tokenResponse = await context.adapter.getUserToken(
context,
this.connectionName
);
if (tokenResponse && tokenResponse.token) {
return true;
}
if (!context.activity.value.state) {
return false;
}
tokenResponse = await context.adapter.getUserToken(
context,
this.connectionName,
context.activity.value.state
);
if (tokenResponse && tokenResponse.token) {
return true;
}
return false;
}
async getSignInResponse(context) {
const signInLink = await context.adapter.getSignInLink(context, this.connectionName);
return {
composeExtension: {
type: 'auth',
suggestedActions: {
actions: [{
type: 'openUrl',
value: signInLink,
title: 'Please sign in'
}]
},
}
};
}
}
I'm playing with Callable HTTPS-functions in iOS. I've created and deployed the following function:
export const generateLoginToken = functions.https.onCall((data, context) => {
const uid = data.user_id
if (!(typeof uid === 'string') || uid.length === 0) {
throw new functions.https.HttpsError('invalid-argument', 'The function must be called with one argument "user_id" ');
}
admin.auth().createCustomToken(uid)
.then((token) => {
console.log("Did create custom token:", token)
return { text: "some_data" };
}).catch((error) => {
console.log("Error creating custom token:", error)
throw new functions.https.HttpsError('internal', 'createCustomToken(uid) has failed for some reason')
})
})
Then I call the function from my iOS-app like this:
let callParameters = ["user_id": userId]
self?.functions.httpsCallable("generateLoginToken").call(callParameters) { [weak self] (result, error) in
if let localError = self?.makeCallableFunctionError(error) {
single(SingleEvent.error(localError))
} else {
print("Result", result)
print("data", result?.data)
if let text = (result?.data as? [String: Any])?["text"] as? String {
single(SingleEvent.success(text))
} else {
let error = NSError.init(domain: "CallableFunctionError", code: 3, userInfo: ["info": "didn't find custom access token in the returned result"])
single(SingleEvent.error(error))
}
}
}
I can see on the logs that the function is invoked on the server with the right parameters, but I can't seem to the get data that is being returned from the function back into the app. It seems that the result.data value is nilfor some reason, even though I return {text: "some_data"} from the cloud function. How come?
Yikes! The issue was that I forgot to return the actual promise from the cloud function. This function is working:
export const generateLoginToken = functions.https.onCall((data, context) => {
const uid = data.user_id
if (!(typeof uid === 'string') || uid.length === 0) {
throw new functions.https.HttpsError('invalid-argument', 'The function must be called with one argument "user_id" ');
}
return admin.auth().createCustomToken(uid)
.then((token) => {
console.log("Did create custom token:", token)
return { text: "some_data" };
}).catch((error) => {
console.log("Error creating custom token:", error)
throw new functions.https.HttpsError('internal', 'createCustomToken(uid) has failed for some reason')
})
})
Can i fix my problems and what wrong in my code?
Problems
i use application for call to my clients from twilio number
When my managers call to US phone number, client see incoming call as "Unknown name"
When my manager calls - silence before connect to client in browser
Then client accepts call - and all ok
Server code
generateToken: () {
let clientName = 'twilio_agent';
let _id = req.body._id;
let phoneno = req.body.phoneno;
//
realmsProvider.get({_id: _id})
.then(data => {
if (!data) {
return res.status(404).json({ message: 'Account not found with _id:'+ _id });
}
var capability = new ClientCapability({
accountSid: data.sid,
authToken: data.authToken,
ttl: config.twilio.ttl
});
capability.addScope(
new ClientCapability.OutgoingClientScope({
applicationSid: data.application.sid,
clientName: data.name,
callerId: phoneno
}));
// incoming call
if (phoneno != null) {
capability.addScope(
new ClientCapability.IncomingClientScope(phoneno));
}
else {
capability.addScope(
new ClientCapability.IncomingClientScope(clientName));
}
var token = capability.toJwt();
return res.status(200).json({ token: token });
})
.catch(err => {
return utils.handleError(res, err);
});
}
voice: () {
let phoneNumber = req.body.PhoneNumber;
let callerId = req.body.CallerId;
let twiml = new VoiceResponse();
let dial = twiml.dial({
//https://www.twilio.com/docs/api/twiml/dial
answerOnBridge: true,
//phone number from
callerId : callerId,
//record all calls
record: 'record-from-answer-dual'
});
if (phoneNumber != null) {
dial.number(phoneNumber);
}
dial.client(callerId.replace(/\+/, ''));
res.type('text/xml');
res.send(twiml.toString());
}
Browser code
Twilio.Device.setup(token, {
debug: true,
enableRingingState: true,
sounds: {
incoming: 'URL-TO-MP3.mp3',
outgoing: 'URL-TO-MP3.mp3',
dtmf8: 'URL-TO-MP3.mp3'
}
});
function call(currentPhoneno, phoneTo, companyNameInTwilio) {
$scope.calling = 1;
if (!validatePhoneno(phoneTo)) {
growl.error('Bad phone number');
return;
}
var connection = Twilio.Device.connect({ // Connect our call.
CallerId: currentPhoneno, // Your Twilio number (Format: +15556667777).
PhoneNumber: phoneTo, // Number to call (Format: +15556667777).
From: companyNameInTwilio
});
connection.on('ringing', function(hasEarlyMedia) {
// some code for ringing
});
}
How i can make redirect if User -id(this.authTokenService.currentUserData.id) have shop (owner_id) .
Shop.component.ts
owner_id :number;
private sub: any;
ngOnInit() {
this.sub = this.httpService.getShops().subscribe(params => {
this.owner_id = this.authTokenService.currentUserData.id ;
console.log(this.sub)
console.log(this.owner_id)
});
if (this.sub) {
this.router.navigate(['/profile']);
}
Http.service.ts
getShops(){
return this.http.get('http://localhost:3000/shops.json')
}
I use Rails 5 for api and auth token. Thanks for help. Sorry for my English
I can't complete grasp your intentions here, but this code seems to be what you're after. Hope this helps
ngOnInit() {
this.getUserShops(this.authTokenService.currentUserData.id)
.subscribe((ownerShops) => {
if (ownerShops.length > 0) {
// User has at least 1 shop
this.router.navigate(['/profile']);
} else {
// User has no shops
}
})
}
getUserShops(ownerId: number): Observable<any> {
return this.httpService
.getShops()
// Map the returned array to a filtered subset including only the owners id
.map((shops: any[]) => shops.filter(shop => shop.owner_id === ownerId));
}
// http.service.ts
export class HttpService {
getShops(): Observable<Shop[]> {
return this.http.get('http://localhost:3000/shops.json')
.map((res: Response) => res.json())
}
}
export interface Shop {
owner_id: number | null;
}
EDIT: Added update to show example http.service.ts typings
private sub: any;
ngOnInit() {
this.httpService.getShops()
.subscribe(params => {
// code here is executed when the response arrives from the server
this.owner_id = this.authTokenService.currentUserData.id ;
console.log(this.sub)
console.log(this.owner_id)
if (this.sub) {
this.router.navigate(['/profile']);
}
});
// code here is executed immediately after the request is sent
// to the server, but before the response arrives.
}
If the code depends on sub that you get from subscribe, then you need to move the code inside the subscribe callback, otherwise it will be executed before this.sub gets a value.
try this code.
owner_id :number;
private sub: any;
ngOnInit() {
this.httpService.getShops().subscribe(
response => {
this.sub = response
if (this.sub.length>0) {
this.router.navigate(['/profile']);
}
}
});