I have created an e-commerce Android app with Firebase and a Flutter app to update the Firebase nodes.
I want to send a notification to all users whenever the admin updates the exchange rate.
Below is the code I wrote to send the update notification to all registered tokens using a Cloud Function but it only sends the notification to the Flutter app instead of both apps.
I don't know what I am doing wrong.
Here is my functions code:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.helloWorld=functions.database.ref('Rates').onUpdate(evt =>{
const payload={
notification:{
title:"Today's Rate",
body :"Click here to see today's gold rate",
badge: '1',
sound: 'default'
}
}
return admin.database().ref('fcm-token').once('value').then(allToken =>{
if(allToken.val()){
console.log('token available');
const token = Object.keys(allToken.val());
console.log(admin.messaging().sendToDevice(token, payload));
return admin.messaging().sendToDevice(token, payload);
}else{
console.log('No Token Available');
throw new Error("Profile doesn't exist")
}
});
});
This is an image of my Realtime Database Structure.
Based on your provided screenshot of your database, the data below /fcm-token in your database has two different formats.
When you added the data manually, you used the format:
{
"fcm-token": {
"someRegistrationToken": {
"token": "someRegistrationToken"
},
"someOtherRegistrationToken": {
"token": "someOtherRegistrationToken"
}
...
},
...
}
Whereas, from your "auto registered" entries, you added the tokens using:
{
"fcm-token": {
"somePushId": {
"fcmToken": "someRegistrationToken"
},
"someOtherPushId": {
"fcmToken": "someOtherRegistrationToken"
},
...
},
...
}
In your Cloud Functions code, you collect all the keys stored under /fcm-token into an array using Object.keys(allToken.val()) which will give you an array containing a mix of push IDs and FCM tokens which is not what you want and is why some devices are missing notifications.
So in short, decide on one format or the other.
Temporary work-around
With your existing mixed structure, you can use the following that will ignore what you use as the key and only extracts the token:
return admin.database().ref('fcm-token').once('value').then(allTokensSnapshot => {
if (allTokensSnapshot.exists()) {
console.log('Tokens available');
const tokenArray = [];
allTokensSnapshot.forEach((tokenSnapshot) => {
let token = tokenSnapshot.hasChild('fcmToken')
? tokenSnapshot.child('fcmToken').val()
: tokenSnapshot.child('token').val();
tokenArray.push(token);
});
return admin.messaging().sendToDevice(tokenArray, payload);
} else {
console.log('No tokens available');
throw new Error('No tokens available');
}
});
Database flattening
Personally, I'd flatten it out so you can use your code as-is, but this would require changing the way you add tokens to the database:
Database structure:
{
"fcm-token": {
"someRegistrationToken": true,
"someOtherRegistrationToken": true,
...
}
}
(You could also use the device owner's user ID instead of true if desired)
Client code:
FirebaseInstanceId.getInstance().getInstanceId()
.addOnSuccessListener(new OnSuccessListener<InstanceIdResult>() {
#Override
public void onSuccess(InstanceIdResult result) {
DatabaseReference allTokensRef = FirebaseDatabase.getInstance().getReference().child("fcm-token");
String token = result.getToken();
Log.e("Test", "FCM Registration Token: " + token);
SharedPreferences preferences = this.getSharedPreferences("com.jk.jkjwellers", MODE_PRIVATE);
if(!(preferences.getBoolean("FCMAdded", false))){
allTokensRef.child(token).setValue(true);
preferences.edit().putBoolean("FCMAdded",true).apply();
}
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.e("Test", "Failed to get FCM registration token", e);
}
});
Not sure, but try replacing
databaseReference.push().setValue(new Tokens(token));
with
databaseReference.child(token).setValue(new Tokens(token));
Related
I am trying to implement push notifications with react and nodejs using service workers.
I am having problem while i am showing notification to the user.
Here is my service worker code:
self.addEventListener('push', async (event) => {
const {
type,
title,
body,
data: { redirectUrl },
} = event.data.json()
if (type === 'NEW_MESSAGE') {
try {
// Get all opened windows that service worker controls.
event.waitUntil(
self.clients.matchAll().then((clients) => {
// Get windows matching the url of the message's coming address.
const filteredClients = clients.filter((client) => client.url.includes(redirectUrl))
// If user's not on the same window as the message's coming address or if it window exists but it's, hidden send notification.
if (
filteredClients.length === 0 ||
(filteredClients.length > 0 &&
filteredClients.every((client) => client.visibilityState === 'hidden'))
) {
self.registration.showNotification({
title,
options: { body },
})
}
}),
)
} catch (error) {
console.error('Error while fetching clients:', error.message)
}
}
})
self.addEventListener('notificationclick', (event) => {
event.notification.close()
console.log(event)
if (event.action === 'NEW_MESSAGE') {
event.waitUntil(
self.clients.matchAll().then((clients) => {
if (clients.openWindow) {
clients
.openWindow(event.notification.data.redirectUrl)
.then((client) => (client ? client.focus() : null))
}
}),
)
}
})
When new notification comes from backend with a type of 'NEW_MESSAGE', i get the right values out of e.data and try to use them on showNotification function but it seems like something is not working out properly because notification looks like this even though event.data equals to this => type = 'NEW_MESSAGE', title: 'New Message', body: , data: { redirectUrl: }
Here is how notification looks:
Thanks for your help in advance.
The problem was i assigned parameters in the wrong way.
It should've been like this:
self.registration.showNotification(title, { body })
I'm new to React/Node and working on a learning project. It's a platform that connects users (freelancers) with nonprofit companies. I would like users to sign up and login as A) user or B) company. I can't figure out how to do this, and all the guides I found are for when your users are all coming from the same table, but with different auth levels (eg. user, admin, etc..).
In my case, it's different. users and companies are two different resources. A user can view /companies and click a button to connect to that company. A user can view a page that lists all their connections. Likewise, a company can login and view a page that lists all the users that connected with them.
Right now, the backend is working successfully. Both users/companies can signup/login, and you get a token back as expected (tested in Insomnia). I'm using JSON Web Tokens.
On the frontend, users can signup, login, make connections, and view their connections successfully. Now I just want companies to do the same, but have no idea how. I made an attempt at doing it, but when a company tries to login, they're directed to the homepage and they're not logged in. No error messages show up.
Not sure what code to post, but I will keep this concise. This is all the relevant code (shortened). I would appreciate any help, or pointers.
schema
CREATE TABLE companies (
company_handle VARCHAR(25) PRIMARY KEY,
password TEXT NOT NULL,
company_name TEXT NOT NULL
role TEXT DEFAULT 'company'
);
CREATE TABLE users (
username VARCHAR(25) PRIMARY KEY,
password TEXT NOT NULL,
role TEXT DEFAULT 'user'
);
CREATE TABLE connections (
username VARCHAR(25)
REFERENCES users ON DELETE CASCADE,
company_handle VARCHAR(25)
REFERENCES companies ON DELETE CASCADE,
PRIMARY KEY (username, company_handle)
);
Frontend
App.js
function App() {
const [currentUser, setCurrentUser] = useState(null);
const [currentCompany, setCurrentCompany] = useState(null);
const [token, setToken] = useLocalStorage(TOKEN_LOCAL_STORAGE_ID);
const [connectionHandles, setConnectionHandles] = useState([]);
// Load user info from the API
useEffect(function loadUserInfo() {
async function getCurrentUser() {
if (token) {
try {
let { username } = jwt.decode(token);
let { companyHandle } = jwt.decode(token);
VolunteerApi.token = token;
if (username) {
let currentUser = await VolunteerApi.getCurrentUser(username);
setCurrentUser(currentUser);
}
if (companyHandle) {
let currentCompany = await VolunteerApi.getCurrentCompany(companyHandle);
setCurrentCompany(currentCompany);
}
} catch (err) {
console.error("Problem with the loadUserInfo function", err);
setCurrentUser(null);
setCurrentCompany(null);
}
}
}
getCurrentUser();
}, [token]);
// Login user function
async function loginUser(loginData) {
try {
let token = await VolunteerApi.loginUser(loginData);
setToken(token);
return {
success: true
};
} catch (err) {
console.error("Problem with the login function", err);
return {
success: false, err
};
}
}
// Login company function
async function loginCompany(loginData) {
try {
let token = await VolunteerApi.loginCompany(loginData);
setToken(token);
return {
success: true
};
} catch (err) {
console.error("Problem with the login function", err);
return {
success: false, err
};
}
}
return (
<BrowserRouter>
<UserContext.Provider value={{ connectionHandles, setConnectionHandles, currentUser, setCurrentUser, currentCompany, setCurrentCompany }}>
<div>
<Navigation />
<Routes loginUser={loginUser} loginCompany={loginCompany} />
</div>
</UserContext.Provider>
</BrowserRouter>
);
}
api.js
class VolunteerApi {
static token;
static async request(endpoint, data = {}, method = "get") {
console.debug("API Call:", endpoint, data, method);
const url = `${BASE_URL}/${endpoint}`;
const headers = { Authorization: `Bearer ${VolunteerApi.token}` };
const params = (method === "get")
? data
: {};
try {
return (await axios({ url, method, data, params, headers })).data;
} catch (err) {
console.error("API Error:", err.response);
let message = err.response.data.error.message;
throw Array.isArray(message) ? message : [message];
}
}
// Login company
static async loginCompany(data) {
let res = await this.request(`auth/login-company`, data, "post");
return res.token;
}
// Login user
static async loginUser(data) {
let res = await this.request(`auth/login-user`, data, "post");
return res.token;
}
}
Backend
auth.js
router.post("/login-company", async function (req, res, next) {
try {
const { companyHandle, password } = req.body;
const company = await Company.authenticate(companyHandle, password);
const token = createToken(company);
return res.json({ token });
} catch (err) {
return next(err);
}
});
router.post("/login-user", async function (req, res, next) {
try {
const { username, password } = req.body;
const user = await User.authenticate(username, password);
const token = createToken(user);
return res.json({ token });
} catch (err) {
return next(err);
}
});
token.js
function createToken(user) {
console.assert(undefined,
"createToken passed user with an undefined user");
let payload = {
username: user.username,
companyHandle: user.companyHandle
};
return jwt.sign(payload, SECRET_KEY);
}
If I understand correctly what you wish to achieve is that your same app can be viewed with 2 different perspectives (User view or Company view) using who logged in as your flag to show the correct data. Having different roles for the same page can be tricky but thankfully there are a number of ways to achieve this.
What I recommend as the simplest approach would be conditional rendering.
When someone logs in as a user or a company you can save that detail to the browsers local storage using localStorage.setItem("UserType", "Example"); and you can get this information using localStorage.getItem("UserType");
Then when the user or company is in your page using that detail you can render the right elements like so:
{condition == true && (<> <Module/> </>)}
Now since we are using react we can import whole js files as modules. so you can have something that looks like this:
import UserPage from 'somewhere/User.js'
import CompanyPage from 'somewhere/Company.js'
function MainApp() {
const userOrCompany = localStorage.getItem("UserType")
return(
<>
{userOrCompany === 'User' && (<> <UserPage/> </>)}
{userOrCompany === 'Company' && (<> <CompanyPage/> </>)}
</>
);
}
export default MainApp;
Also, I recommend handling tokens from the backend for security reasons. That way you can condition your backend data to needing a token before returning anything :D
I'm creating an Apollo Client like this:
var { ApolloClient } = require("apollo-boost");
var { InMemoryCache } = require('apollo-cache-inmemory');
var { createHttpLink } = require('apollo-link-http');
var { setContext } = require('apollo-link-context');
exports.createClient = (shop, accessToken) => {
const httpLink = createHttpLink({
uri: `https://${shop}/admin/api/2019-07/graphql.json`,
});
const authLink = setContext((_, { headers }) => {
return {
headers: {
"X-Shopify-Access-Token": accessToken,
"User-Agent": `shopify-app-node 1.0.0 | Shopify App CLI`,
}
}
});
return new ApolloClient({
cache: new InMemoryCache(),
link: authLink.concat(httpLink),
});
};
to hit the Shopify GraphQL API and then running a query like that:
return client.query({
query: gql` {
productVariants(first: 250) {
edges {
node {
price
product {
id
}
}
cursor
}
pageInfo {
hasNextPage
}
}
}
`})
but the returned object only contain data and no extensions which is a problem to figure out the real cost of the query.
Any idea why?
Many thanks for your help
There's a bit of a hacky way to do it that we wrote up before:
You'll need to create a custom apollo link (Apollo’s equivalent of middleware) to intercept the response data as it’s returned from the server, but before it’s inserted into the cache and the components re-rendered.
Here's an example were we pull metrics data from the extensions in our API:
import { ApolloClient, InMemoryCache, HttpLink, ApolloLink } from 'apollo-boost'
const link = new HttpLink({
uri: 'https://serve.onegraph.com/dynamic?show_metrics=true&app_id=<app_id>',
})
const metricsWatchers = {}
let id = 0
export function addMetricsWatcher(f) {
const watcherId = (id++).toString(36)
metricsWatchers[watcherId] = f
return () => {
delete metricsWatchers[watcherId]
}
}
function runWatchers(requestMetrics) {
for (const watcherId of Object.keys(metricsWatchers)) {
try {
metricsWatchers[watcherId](requestMetrics)
} catch (e) {
console.error('error running metrics watcher', e)
}
}
}
// We intercept the response, extract our extensions, mutatively store them,
// then forward the response to the next link
const trackMetrics = new ApolloLink((operation, forward) => {
return forward(operation).map(response => {
runWatchers(
response
? response.extensions
? response.extensions.metrics
: null
: null
)
return response
})
})
function create(initialState) {
return new ApolloClient({
link: trackMetrics.concat(link),
cache: new InMemoryCache().restore(initialState || {}),
})
}
const apolloClient = create(initialState);
Then to use the result in our React components:
import { addMetricsWatcher } from '../integration/apolloClient'
const Page = () => {
const [requestMetrics, updateRequestMetrics] = useState(null)
useEffect(() => {
return addMetricsWatcher(requestMetrics =>
updateRequestMetrics(requestMetrics)
)
})
// Metrics from extensions are available now
return null;
}
Then use a bit of mutable state to track each request and its result, and the use that state to render the metrics inside the app.
Depending on how you're looking to use the extensions data, this may or may not work for you. The implementation is non-deterministic, and can have some slight race conditions between the data that’s rendered and the data that you've extracted from the extensions.
In our case, we store performance metrics data in the extensions - very useful, but ancillary - so we felt the tradeoff was acceptable.
There's also an open issue on the Apollo client repo tracking this feature request
I dont have any idea of ApolloClient but i tried to run your query in shopify graphql app. It return results with extensions. Please find screenshot below. Also You can put questions in ApolloClient github.
I am using firebase admin sdk with nodejs to manage users accounts, specifically, migrating from one firebase to another.
Though the migration ran well, I noticed a problem when I decided to delete all the users that were migrated and run the migration again.
This is the code that migrates auth users:
async saveAuthUsers(authUsers) {
try {
const hash = {
hash: {
algorithm: 'STANDARD_SCRYPT',
memoryCost: 1024,
parallelization: 16,
blockSize: 8,
derivedKeyLength: 64
}
};
const userImportResult = await this.firebaseTo.auth().importUsers(authUsers, hash);
// failed auth users
if (userImportResult.failureCount) {
userImportResult.errors.forEach((indexedError) => {
console.log('User: ' + authUsers[indexedError.index].uid + ' with email: ' + authUsers[indexedError.index].email + ' failed to import ', indexedError.error);
});
}
authUsers.forEach((authUser) => {
console.log(`Current migrated user uid: ${authUser.uid} email: ${authUser.email}`);
});
} catch (error) {
console.log(error);
}
}
Is there any reason why this could happen?
I am using FeathersJS and been happy with authentication it provides. I this case it is local JWT. A client requested user management with an ability to disable some. There is field isDisabled in Users model, but it's hard to figure out where the check should be performed and how to set it up.
"#feathersjs/feathers": "^3.0.2",
"#feathersjs/authentication": "^2.1.0",
"#feathersjs/authentication-jwt": "^1.0.1",
"#feathersjs/authentication-local": "^1.0.2",
It depends where you want to check. You can either customize the JWT verifier or create a hook on the users service for the get method:
app.service('users').hooks({
after: {
get(context) {
const user = context.result;
if(user.isDisabled) {
throw new Error('This user has been disabled');
}
}
}
});
I did this directly in my authenticate hook:
const { authenticate } = require('#feathersjs/authentication').hooks
const { NotAuthenticated } = require('#feathersjs/errors')
const verifyIdentity = authenticate('jwt')
function hasToken(hook) {
if (hook.params.headers == undefined) return false
if (hook.data.accessToken == undefined) return false
return hook.params.headers.authorization || hook.data.accessToken
}
module.exports = async function authenticate(context) {
try {
await verifyIdentity(context)
} catch (error) {
if (error instanceof NotAuthenticated && !hasToken(context)) {
return context
}
}
if (context.params.user && context.params.user.disabled) {
throw new Error('This user has been disabled')
}
return context
}
You see I did check the just loaded user record and throw an error in case. And as this hook is called in before:all the user is rejected before any action is done.
As for feathers 4 you can extend your auth strategies very easily. For example if we want to user only be able to login and verify their JWT we would do the following in authentication.ts (Typescript):
import { Id, Query, ServiceAddons } from '#feathersjs/feathers';
import { AuthenticationService, JWTStrategy } from '#feathersjs/authentication';
import { LocalStrategy } from '#feathersjs/authentication-local';
import { expressOauth } from '#feathersjs/authentication-oauth';
import { Application } from './declarations';
declare module './declarations' {
interface ServiceTypes {
'authentication': AuthenticationService & ServiceAddons<any>;
}
}
Extend the local strategy by alter getEntityQuery to only inlcude users which are active.
class CustomLocalStrategy extends LocalStrategy {
async getEntityQuery(query: Query) {
return {
...query,
active: true,
$limit: 1
};
}
}
Extend the JWT strategy by alter getEntity() to return null if the user is inactive
class CustomJWTStrategy extends JWTStrategy {
async getEntity(id: Id) {
const entity = await this.entityService.get(id);
if (!entity.active) {
return null;
}
return entity;
}
}
export default function(app: Application): void {
const authentication = new AuthenticationService(app);
authentication.register('jwt', new CustomJWTStrategy());
authentication.register('local', new CustomLocalStrategy());
app.use('/authentication', authentication);
app.configure(expressOauth());
}