I'm integrating the Stripe checkout in my web app. I've implemented Stripe checkout on the angular front end and have also created a backend that is supposed to receive the token passed by stripe checkout. Upon submission of the stripe checkout form, my POST http request is not passing data to the backend. Although I get a 200 status from Stripe, I get no response for my nodejs.
Here is my checkout method invoked by a form.
openCheckout() {
let total = (this.donation * 100);
let handler = (<any>window).StripeCheckout.configure({
key: 'key_test',
locale: 'auto',
token: (token: any) => {
const transaction = new Charge(total, token.id);
console.log('From navbar nonObject ' + token.id + ' ' + total);
console.log(transaction + ' From navbar');
this.keyService.charge(transaction);
}
});
handler.open({
name: 'Delaware March for Jesus',
description: 'Donation',
amount: total
});
this.donation = 0;
this.donationEmail = '';
}
Here is my Service code that implements the charge and passes the token to the backend.
charge(transaction: Charge) {
const body = JSON.stringify(transaction);
const headers = new Headers({'Content-type': 'application/json'});
return this.http.post(this.keysUrlDev + '/charge', body, { headers: headers })
.map((response: Response) => response.json())
.catch((error: Response) => Observable.throw(error.json()));
}
A simple angular model I constructed for transactions.
export class Charge {
constructor(public amount: number,
public token: string) {}
}
And my POST route on nodejs that takes the token and passes it through the stripe library charge.create method.
router.post('/charge', function(req, res, next) {
var amount = req.body.amount;
var token = req.body.token;
stripe.charges.create({
amount: amount,
currency: 'usd',
description: 'Delaware March For Jesus Donation',
source: token
}, function(err, charge) {
if (err) {
console.log(req.body.amount + ' From POST' + req.body.token);
return res.status(500).json({
title: 'An error occured',
error: err
});
}
res.status(201).json({
message: 'Charged successfully',
obj: charge
});
});
});
I've gotten the token from the front end and sent a POST request via Postman successfully. Which logs the successful transaction and shows it in my stripe account. But, none of that happens when sending the request via angular.
I've used console.log to trace where the code stops and I can retrieve the token and amount in the keyService charge method. So it must be http.post that is not working properly.
I was having the same type of issue almost exactly, and the change that made this work for me was to remove the InMemoryWebApiModule.forRoot(InMemoryDataService), and build the API into my express backend as a straightforward cut-and-paste. (I originally used the Angular 'Heroes' tutorial, and the in-memory API was implemented there.)
If I understand the issue broadly, it is that this in-memory API took over from the regular functioning of Angular's HTTP. Once released it functioned normally again - though I can't be more technically specific.
I hope it works for you too.
Related
I have followed the example in Display the Sign In With Google button to get a Google sign in button working in my Angular application:
<div id="g_id_onload"
class="mt-3"
data-client_id="XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com"
data-login_uri="http://localhost:1337/login/google"
data-auto_prompt="false">
</div>
<div class="g_id_signin"
data-width="250"
data-type="standard"
data-size="large"
data-theme="outline"
data-text="continue_with"
data-shape="rectangular"
data-logo_alignment="center">
</div>
Once the user signs in, I verify and decode the JWT token provided by Google in my Express server using jsonwebtoken:
app.post('/login/google', express.urlencoded(), async(request, response, next) => {
try {
console.log(`${request.method} ${request.url} was called.`);
let token: string = request.body.credential;
let body: Response = await fetch('https://www.googleapis.com/oauth2/v1/certs', { method: 'GET', headers: { Accept: 'application/json' }});
let json: any = await body.json();
let certificates: string[] = Object.keys(json).map(key => json[key]);
let decoded: any;
let lastError: any;
certificates.every(certificate => {
try {
decoded = jwt.verify(token, certificate, { algorithms: ['RS256'], ignoreExpiration: false });
}
catch (error) {
lastError = error;
}
return !decoded;
});
if (!decoded)
throw lastError;
}
catch (error) {
next(error);
}
});
The problem is that the decoded token does not contain the user's gender or birthday information. How can I obtain this data?
I have just recently tried manually appending the https://www.googleapis.com/auth/user.birthday.read and https://www.googleapis.com/auth/user.gender.read scopes to my application's OAuth Consent Screen found at https://console.cloud.google.com/apis/credentials/consent/edit, but I don't see the user being prompted to provide this data to my application when it runs. I tried deleting permissions to my application from my account at accounts.google.com (under the Third-Party Access section) as well in hopes that it might prompt for these extra pieces of data. I am not sure at this point how to go about getting this extra data because I can't seem to find a good documentation piece on how to achieve this. Also, I wanted to add that my test account's Gender and Birthday information is set to be Private in https://myaccount.google.com/personal-info. I was wondering if its possible to fetch these private scopes somehow.
So, just to be clear, when I try to sign in I still only get the following prompt, which makes me believe that something is wrong and its not actually requesting the scope for birthday and gender from the user:
Confirm you want to sign in to [Application Name] with [User's Name].
To create your account, Google will share your name, email address,
and profile picture with [Application Name].
I also tried going on https://developers.google.com/oauthplayground/ and I pasted this in for Input your own scopes: https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/userinfo.profile,https://www.googleapis.com/auth/user.birthday.read,https://www.googleapis.com/auth/user.gender.read. I then hit the Authorize API button, logged in, granted access to these scopes (was prompted correctly on the playground), performed the token exchange, then I tried to List possible operations and under the People API, I called the get people endpoint, and modified the URI to https://people.googleapis.com/v1/people/me as per the documentation. This endpoint seems to work to fetch the data I need, but now I can't seem to wrap my head around what authorization parameters to use for this endpoint from the data I get back from the POST to my Express server. I have also tried enabling the People API from Enabled APIs & services.
You are using signin. Signin is open id connect and returns an id token. Id tokes contain very few claims. Gender is not one of them.
The only way to get access to the full user profile info is to go though the people api as you have mentioned.
You can use the try me to see it working and generate the sample for you.
<script src="https://apis.google.com/js/api.js"></script>
<script>
/**
* Sample JavaScript code for people.people.get
* See instructions for running APIs Explorer code samples locally:
* https://developers.google.com/explorer-help/code-samples#javascript
*/
function authenticate() {
return gapi.auth2.getAuthInstance()
.signIn({scope: "https://www.googleapis.com/auth/contacts https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/directory.readonly https://www.googleapis.com/auth/user.addresses.read https://www.googleapis.com/auth/user.birthday.read https://www.googleapis.com/auth/user.emails.read https://www.googleapis.com/auth/user.gender.read https://www.googleapis.com/auth/user.organization.read https://www.googleapis.com/auth/user.phonenumbers.read https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"})
.then(function() { console.log("Sign-in successful"); },
function(err) { console.error("Error signing in", err); });
}
function loadClient() {
gapi.client.setApiKey("YOUR_API_KEY");
return gapi.client.load("https://people.googleapis.com/$discovery/rest?version=v1")
.then(function() { console.log("GAPI client loaded for API"); },
function(err) { console.error("Error loading GAPI client for API", err); });
}
// Make sure the client is loaded and sign-in is complete before calling this method.
function execute() {
return gapi.client.people.people.get({
"resourceName": "people/me",
"personFields": "genders"
})
.then(function(response) {
// Handle the results here (response.result has the parsed body).
console.log("Response", response);
},
function(err) { console.error("Execute error", err); });
}
gapi.load("client:auth2", function() {
gapi.auth2.init({client_id: "YOUR_CLIENT_ID"});
});
</script>
<button onclick="authenticate().then(loadClient)">authorize and load</button>
<button onclick="execute()">execute</button>
The issue that you are then going to have is the above sample uses Oauth2 and not open id connect (signin) It needs an access token to work. If you check your code I belive that the signin does return an access token. Your job then is to feed the access token to the code above so that you dont have to go though the authorization process again.
So far i have not found anyone able to link the new signin system with the old oauth2 system. If you get it to work i would love to see it.
Html
To call this api you need an access_token. a google access token is not a jwt. it is not the id_token
GET https://people.googleapis.com/v1/people/me?personFields=genders&key=[YOUR_API_KEY] HTTP/1.1
Authorization: Bearer [YOUR_ACCESS_TOKEN]
Accept: application/json
I finally managed to get it working with the help of this guide.
I had to scrap the idea of using the Google sign in button because it does not seem to allow extended scopes such as birthday and gender (well, not if they're private anyways - if anyone finds a way of doing it with the sign in button, please post an answer). Luckily, their OAuth API does support extended scopes. As such, I've implemented my own Google sign in button using the googleapis package.
There are a few steps to this:
Use the googleapis package to generate a URI to present to the user that will ask them to consent to gender and birthday access.
For example:
app.get('/login/google/uri', async(request, response, next) => {
try {
console.log(`${request.method} ${request.url} was called.`);
let client = new google.auth.OAuth2(
'ClientID',
'ClientSecret',
`http://localhost:4200/login/google/redirect`
);
const scopes = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/user.birthday.read',
'https://www.googleapis.com/auth/user.gender.read'
];
const authorizationUrl: string = client.generateAuthUrl({
access_type: 'offline',
scope: scopes,
include_granted_scopes: false
});
response.status(200).send({ uri: authorizationUrl });
}
catch (error) {
next(error);
}
});
Ensure that http://localhost:4200/login/google/redirect (or whatever redirect URI you use) is part of your OAuth 2.0 Client ID Credential's Authorized redirect URIs in the console.
Google will redirect to your redirect URI (http://localhost:4200/login/google/redirect) with a query parameter named code. For example: http://localhost:4200/login/google/redirect?code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&scope=email%20profile%20https:%2F%2Fwww.googleapis.com%2Fauth%2Fuser.gender.read%20https:%2F%2Fwww.googleapis.com%2Fauth%2Fuser.birthday.read%20https:%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email%20https:%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile%20openid&authuser=0&prompt=consent
Take the code (XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX) and exchange it for an access token.
For example:
let client = new google.auth.OAuth2(
'ClientID',
'ClientSecret',
`http://localhost:4200/login/google/redirect`
);
let code: string = request.params.code;
let { tokens } = await client.getToken(code);
console.log(tokens.access_token);
Use the access_token (it looks something like XXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX) when making requests to the People API and set it in the Authorization header as the bearer token.
For example:
curl "https://people.googleapis.com/v1/people/me?key=XXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXX&personFields=genders,birthdays" -H "Authorization: Bearer XXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
key is your API key from the console (you can create one and restrict it to the People API - if you don't see the People API as a restriction option you might need to enable it from the Enabled APIs and services tab). I'm sure there is a more API friendly way of making this request in the googleapis package that you can explore, but I just wanted to highlight how it works with curl.
The response you will see should be like this:
{
"resourceName": "people/XXXXXXXXXXXXXXXXXXXX",
"etag": "XXXXXXXXXXXXXXXXXXXXX",
"genders": [
{
"metadata": {
"primary": true,
"source": {
"type": "PROFILE",
"id": "XXXXXXXXXXXXXXXXXXXX"
}
},
"value": "male",
"formattedValue": "Male"
}
],
"birthdays": [
{
"metadata": {
"primary": true,
"source": {
"type": "ACCOUNT",
"id": "XXXXXXXXXXXXXXXXXXXX"
}
},
"date": {
"year": 1901,
"month": 1,
"day": 1
}
}
]
}
Edit: Just for completion, here is the API friendly way to do all of this.
First, generate this URI and redirect the user to it:
app.get('/login/google/uri', async(request, response, next) => {
try {
console.log(`${request.method} ${request.url} was called.`);
let client = new googleapis.Auth.OAuth2Client(
Globals.GoogleClientID,
Globals.GoogleClientSecret,
`${Globals.UIHost}/login`
);
const scopes = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/user.birthday.read',
'https://www.googleapis.com/auth/user.gender.read'
];
const authorizationUrl: string = client.generateAuthUrl({
access_type: 'offline',
scope: scopes,
include_granted_scopes: false
});
response.status(200).send({ uri: authorizationUrl });
}
catch (error) {
next(error);
}
});
Second, after the user has signed in and you get a code posted back to your redirect URI, parse the query param for the code and use it like how I am doing so in the following POST method on my server to get these extra user details for birthdays, genders, and emails:
app.post('/login/google', express.json(), async(request, response, next) => {
try {
console.log(`${request.method} ${request.url} was called.`);
let client = new googleapis.Auth.OAuth2Client(
Globals.GoogleClientID,
Globals.GoogleClientSecret,
`${Globals.UIHost}/login`
);
let code: string = request.body.code;
let { tokens } = await client.getToken(code);
let accessToken: string = tokens.access_token;
client.setCredentials({ access_token: accessToken });
let people = new googleapis.people_v1.People({});
let result = await people.people.get({
resourceName: 'people/me',
personFields: 'emailAddresses,birthdays,genders',
auth: client
});
console.log(result.data);
}
catch (error) {
next(error);
}
});
result.data should contain the information.
If you are using NestJS with typescript, this worked for me
#Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(configService: ConfigService) {
super({
clientID: configService.get('GOOGLE_CLIENT_ID'),
clientSecret: configService.get('GOOGLE_SECRET'),
callbackURL: configService.get('GOOGLE_REDIRECT_URL'),
scope: [
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/plus.login',
'https://www.googleapis.com/auth/user.birthday.read',
'https://www.googleapis.com/auth/user.phonenumbers.read',
'https://www.googleapis.com/auth/user.gender.read',
],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const { name, emails, photos, sub, birthday, phoneNumber, gender } =
profile;
const user = {
sub,
email: emails[0].value,
firstName: name.givenName,
lastName: name.familyName,
picture: photos[0].value,
dob: birthday,
phoneNumber,
gender,
refreshToken,
accessToken,
};
done(null, user);
}
}
Then add GoogleStrategy to your provider. Of course, don't forget your keys in your .env file.
I have a paid developer account with Google. I have verified my People API is enabled and that I am using the correct API Key and Client ID. When I try the Google Login Start, I get this error in the console for reason.result.error...
{
code: 401,
message: "The request does not have valid authentication credentials.",
status: "UNAUTHENTICATED"
}
I verified that the API key and ClientID are correct. I start off with a method like this for the login button...
didClickGoogleLogin = () => {
// 1. Load the JavaScript client library.
gapi.load('client', this.googleLoginStart);
}
Then it calls the googleLoginStart method...
googleLoginStart = () => {
// 2. Initialize the JavaScript client library.
gapi.client.init({
'apiKey': <API KEY HERE>,
// clientId and scope are optional if auth is not required.
'clientId': <CLIENT ID HERE>,
'scope': 'profile',
}).then(function() {
// 3. Initialize and make the API request.
return gapi.client.request({
'path': 'https://people.googleapis.com/v1/people/me?personFields=metadata,names,emailAddresses',
})
}).then((response) => {
console.log(response.result);
const googleID = response.result.metadata.sources[0].id;
const googleEmail = response.result.emailAddresses[0].value;
this.updateUser(googleID, googleEmail);
}, function(reason) {
console.log("reason: ", reason);
});
}
This was working for me in another web app I wrote a few months ago, but it's not working there now either.
Any ideas why I'm getting an invalid creds error when the API is enabled, the API Key & Client ID are correct?
I want users to pay a fee before a POST request from a front end form is processed. I have a Stripe webhook that works fine on the backend, but I'm not sure how to delay the front end posting of the form until after the payment confirmation is received.
In the code below, right now, createTour and createTourPay run at the same time. I would like for createTourPay to execute first, and the createTour only triggers after Stripe posts to my application from the webhook. How can I achieve this?
Controller File (webhook):
exports.webhookCheckout = (req, res, next) => {
const signature = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook error: ${err.message}`);
}
if (
event.type === 'checkout.session.completed' &&
event.line_items.name === 'New Job Purchase'
) {
res.status(200).json({ recieved: true });
// Somehow, I want this to trigger the execution of the POST request in my front end JS file.
} else {
if (event.type === 'checkout.session.completed')
createBookingCheckout(event.data.object);
res.status(200).json({ recieved: true });
}
};
Front end JS file:
export const createTourPay = async myForm => {
try {
// 1) Get the checkout session from API response
const session = await axios(`/api/v1/tours/tour-pay`);
const complete = 1;
// console.log(session);
// 2) Create checkout form + charge the credit card
await stripe.redirectToCheckout({
sessionId: session.data.session.id
});
} catch (err) {
// console.log(err);
showAlert('error', err);
}
};
export const createTour = async myForm => {
try {
const startLocation = {
type: 'Point',
coordinates: [-10.185942, 95.774772],
address: '123 Main Street',
description: 'Candy Land'
};
const res = await axios({
method: 'POST',
headers: {
'Content-Type': `multipart/form-data; boundary=${myForm._boundary}`
},
url: '/api/v1/tours',
data: myForm
});
if (res.data.status === 'success') {
showAlert('success', 'NEW TOUR CREATED!');
window.setTimeout(() => {
location.assign('/');
}, 1500);
}
} catch (err) {
showAlert('error', err.response.data.message);
}
};
Broadly: don't do this. Instead, you in fact should create some pending/unpaid version of the "tour" (or any other product/service) in your system, then attach the unique id (eg: tour_123) to the Checkout session when you create it, either using the client_reference_id (doc) or metadata (doc):
const session = await stripe.checkout.sessions.create({
// ... other params
client_reference_id: 'tour_123',
metadata: { tour_id: 'tour_123' },
});
Then you'd use the webhook to inspect those values, and update your own database to indicate the payment has been made and that you can fulfill the order to the customer (ship product, send codes, allow access to service etc).
If you really want to proceed with a more synchronous flow, you can use separate auth and capture to sequence your customer experience and capture the funds later after authorizing and creating your tour entity.
Edit: a note about security
You should never trust client-side logic for restricted operations like creating a "paid" tour. A motivated user could, for example, simply call your /api/v1/tours create endpoint without ever going through your payment flow. Unless you validate a payment and track that state on your server you won't be able to know which of these had actually paid you.
Why am I not getting the response with the capture details on the client side?
I am trying to implement a server side integration for PayPal's smart buttons. I have tried a few different methods, and this is the method I have had the most success with.
However, it still doesn't appear to be working 100%. Atm, clicking a button opens the payment window, I can login with the sandbox personal account, go through the checkout flow, and then I get the standard alert, but for some reason I am not getting the desired response from the server.
When I sign into sandbox paypal, on the personal account, I can see the transactions being sent successfully (they are pending, awaiting confirmation from the merchant). When I sign into the sandbox merchant account, there are no transactions available. When I take the order ID from the smart button, and send it to PayPal's api route to get the order details, it comes back as captured and completed.
Has anyone else experienced something similar with the payments not showing up on the merchant sandbox account? If I sign into the developer account, and look at the API log, I can see the orders being created and captured successfully, but they still don't show up on the merchant account.
Here's my server side code:
const express = require("express");
const router = express.Router();
// 1. Set up your server to make calls to PayPal
// 1a. Import the SDK package
const paypal = require("#paypal/checkout-server-sdk");
// 1b. Import the PayPal SDK client that was created in `Set up Server-Side SDK`.
/**
*
* PayPal HTTP client dependency
*/
const payPalClient = require("./PayPalConfig");
// route to set up a transaction
router.post("/orders/create", async (req, res) => {
// 3. Call PayPal to set up a transaction
const request = new paypal.orders.OrdersCreateRequest();
request.prefer("return=representation");
request.requestBody({
intent: "CAPTURE",
purchase_units: [
{
amount: {
currency_code: "USD",
value: "4.20",
},
},
],
});
let order;
try {
order = await payPalClient.client().execute(request);
} catch (err) {
// 4. Handle any errors from the call
console.error(err);
return res.sendStatus(500);
}
// 5. Return a successful response to the client with the order ID
res.json({
orderID: order.result.id,
});
console.log(order.result.id);
});
// route to handle capturing of orders
router.post("/orders/capture", async (req, res) => {
// const captureDetails
let captureDetails = "";
// 2a. Get the order ID from the request body
const orderID = req.body.orderID;
// 3. Call PayPal to capture the order
const request = new paypal.orders.OrdersCaptureRequest(orderID);
request.requestBody({});
try {
const capture = await payPalClient.client().execute(request);
// 4. Save the capture ID to your database. Implement logic to save capture to your database for future reference.
const captureID = capture.result.purchase_units[0].payments.captures[0].id;
captureDetails = capture.result;
// await database.saveCaptureID(captureID);
res.json(captureDetails);
} catch (err) {
// 5. Handle any errors from the call
console.error(err);
return res.sendStatus(500);
}
// 6. Return a successful response to the client
// res.sendStatus(200).json({ details: captureDetails });
res.json({ details: captureDetails });
});
module.exports = router;
Here's my client side code:
// Render the PayPal button into #paypal-button-container
paypal
.Buttons({
// Call your server to set up the transaction
createOrder: function (data, actions) {
return fetch("http://localhost:3000/payment/paypal/orders/create", {
method: "post",
})
.then(function (res) {
return res.json();
})
.then(function (orderData) {
return orderData.orderID;
console.log(orderData.orderID);
});
},
// Call your server to finalize the transaction
onApprove: function (data) {
return fetch("http://localhost:3000/payment/paypal/orders/capture", {
method: "post",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
orderID: data.orderID,
}),
})
.then(function (res) {
return res;
})
.then(function (details) {
console.log(details);
alert("Transaction funds captured from " + details.payer_given_name);
});
},
})
.render("#paypal-button-container");
Here's the details being logged from the client
Response {type: "cors", url: "http://localhost:3000/payment/paypal/orders/capture", redirected: false, status: 200, ok: true, …}
body: (...)
bodyUsed: false
headers: Headers {}
ok: true
redirected: false
status: 200
statusText: "OK"
type: "cors"
url: "http://localhost:3000/payment/paypal/orders/capture"
__proto__: Response
On the server side, don't specify 'details' as a key.
res.json(captureDetails);
You need to return res.json() on the client side. It hasn't parsed the json object.
When I sign into the sandbox merchant account, there are no
transactions available. When I take the order ID from the smart
button, and send it to PayPal's api route to get the order details, it
comes back as captured and completed.
You are signing in to the wrong sandbox merchant account. The correct one will depend on the sandbox clientId you are using.
I'm working on the Google Chrome Push Notification and I'm trying to send the payload to the google chrome worker but, I have no idea how I receive this payload.
I have an API to create and save the notifications in my database and I need send the values through the https://android.googleapis.com/gcm/send and receive on the worker.js
This is my worker.js
self.addEventListener('push', function(event) {
var title = 'Yay a message.';
var body = 'We have received a push message.';
var icon = '/images/icon-192x192.png';
var tag = 'simple-push-demo-notification-tag';
event.waitUntil(
self.registration.showNotification(title, {
body: body,
icon: icon,
tag: tag
})
);
});
And this is how I'm calling the GCM
curl --header "Authorization: key=AIzaSyDQjYDxeS9MM0LcJm3oR6B7MU7Ad2x2Vqc" --header "Content-Type: application/json" https://android.googleapis.com/gcm/send -d "{ \"data\":{\"foo\":\"bar\"}, \"registration_ids\":[\"APA91bGqJpCmyCnSHLjY6STaBQEumz3eFY9r-2CHTtbsUMzBttq0crU3nEXzzU9TxNpsYeFmjA27urSaszKtA0WWC3yez1hhneLjbwJqlRdc_Yj1EiqLHluVwHB6V4FNdXdKb_gc_-7rbkYkypI3MtHpEaJbWsj6M5Pgs4nKqQ2R-WNho82mnRU\"]}"
I tried to get event.data but, this is undefined.
Does anyone have any idea or sugestion?
Unfortunately it seems like an intended behavior:
A downside to the current implementation of the Push API in Chrome is
that you can’t send a payload with a push message. Nope, nothing. The
reason for this is that in a future implementation, payload will have
to be encrypted on your server before it’s sent to a push messaging
endpoint. This way the endpoint, whatever push provider it is, will
not be able to easily view the content of the push payload. This also
protects against other vulnerabilities like poor validation of HTTPS
certificates and man-in-the-middle attacks between your server and the
push provider. However, this encryption isn’t supported yet, so in the
meantime you’ll need to perform a fetch request to get information
needed to populate a notification.
As stated above, the workaround is to contact back your backend after receiving the push and fetch the stored data on the 3rd party server.
#gauchofunky's answer is correct. With some guidance from the folks on the Chromium dev slack channel and #gauchofunky I was able to piece something together. Here's how to work around the current limitations; hopefully my answer becomes obsolete soon!
First figure out how you're going to persist notifications on your backend. I'm using Node/Express and MongoDB with Mongoose and my schema looks like this:
var NotificationSchema = new Schema({
_user: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
subscriptionId: String,
title: String,
body: String,
sent: { type: Boolean, default: false }
});
Be sure to add an icon if you'd like to alter the icon. I use the same icon every time so mine's hardcoded in the service worker.
Figuring out the correct REST web service took some thought. GET seemed like an easy choice but the call to get a notification causes side effects, so GET is out. I ended up going with a POST to /api/notifications with a body of {subscriptionId: <SUBSCRIPTION_ID>}. Within the method we basically perform a dequeue:
var subscriptionId = req.body.subscriptionId;
Notification
.findOne({_user: req.user, subscriptionId: subscriptionId, sent: false})
.exec(function(err, notification) {
if(err) { return handleError(res, err); }
notification.sent = true;
notification.save(function(err) {
if(err) { return handleError(res, err); }
return res.status(201).json(notification);
});
});
In the service worker we need to for sure get the subscription before we make the fetch.
self.addEventListener('push', function(event) {
event.waitUntil(
self.registration.pushManager.getSubscription().then(function(subscription) {
fetch('/api/notifications/', {
method: 'post',
headers: {
'Authorization': 'Bearer ' + self.token,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
})
.then(function(response) { return response.json(); })
.then(function(data) {
self.registration.showNotification(data.title, {
body: data.body,
icon: 'favicon-196x196.png'
});
})
.catch(function(err) {
console.log('err');
console.log(err);
});
})
);
});
It's also worth noting that the subscription object changed from Chrome 43 to Chrome 45. In Chrome 45 the subscriptionId property was removed, just something to look out for - this code was written to work with Chrome 43.
I wanted to make authenticated calls to my backend so I needed to figure out how to get the JWT from my Angular application to my service worker. I ended up using postMessage. Here's what I do after registering the service worker:
navigator.serviceWorker.register('/service-worker.js', {scope:'./'}).then(function(reg) {
var messenger = reg.installing || navigator.serviceWorker.controller;
messenger.postMessage({token: $localStorage.token});
}).catch(function(err) {
console.log('err');
console.log(err);
});
In the service worker listen for the message:
self.onmessage.addEventListener('message', function(event) {
self.token = event.data.token;
});
Strangely enough, that listener works in Chrome 43 but not Chrome 45. Chrome 45 works with a handler like this:
self.addEventListener('message', function(event) {
self.token = event.data.token;
});
Right now push notifications take quite a bit of work to get something useful going - I'm really looking forward to payloads!
Actually, payload should be implemented in Chrome 50 (release date - April 19, 2016). In Chrome 50 (and in the current version of Firefox on desktop) you can send some arbitrary data along with the push so that the client can avoid making the extra request. All payload data must be encrypted.
Here is the the encryption details from developer : https://developers.google.com/web/updates/2016/03/web-push-encryption?hl=en
I just ran into this problem. Newer versions of firefox and chrome( version 50+) support payload transferring. The dev docs here details the implementation on how this works. An important thing to note is that google GCM or possibly client/chome (I dont know which one) will actually ignore the payload entirely if it is not encrypted.
This website has both client/server implementations of how to do the push and retrieval through service workers. The push library that examples use is merely a wrapper around a normal REST call
service worker example implementation:
self.addEventListener('push', function(event) {
var payload = event.data ? event.data.text() : 'no payload';
event.waitUntil(
self.registration.showNotification('ServiceWorker Cookbook', {
body: payload,
})
);
});
Server example implementation:
var webPush = require('web-push');
webPush.setGCMAPIKey(process.env.GCM_API_KEY);
module.exports = function(app, route) {
app.post(route + 'register', function(req, res) {
res.sendStatus(201);
});
app.post(route + 'sendNotification', function(req, res) {
setTimeout(function() {
webPush.sendNotification(req.body.endpoint, {
TTL: req.body.ttl,
payload: req.body.payload,
userPublicKey: req.body.key,
userAuth: req.body.authSecret,
}).then(function() {
res.sendStatus(201);
});
}, req.body.delay * 1000);
});
};
Client side javascript implementation example of printing out the the required fields.
navigator.serviceWorker.register('serviceWorker.js')
.then(function(registration) {
return registration.pushManager.getSubscription()
.then(function(subscription) {
if (subscription) {
return subscription;
}
return registration.pushManager.subscribe({
userVisibleOnly: true
});
});
}).then(function(subscription) {
var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
authSecret = rawAuthSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
endpoint = subscription.endpoint;
console.log("Endpoint: " + endpoint);
console.log("Key: " + key);
console.log("AuthSecret: " + authSecret);
});
To retrieve that data, you need to parse "event.data.text()" to a JSON object. I'm guessing something was updated since you tried to get this to work, but it works now. Unlucky!
However, since I made it to this post when searching for a solution myself, others would probably like a working answer. Here it is:
// Push message event handler
self.addEventListener('push', function(event) {
// If true, the event holds data
if(event.data){
// Need to parse to JSON format
// - Consider event.data.text() the "stringify()"
// version of the data
var payload = JSON.parse(event.data.text());
// For those of you who love logging
console.log(payload);
var title = payload.data.title;
var body = payload.data.body;
var icon = './assets/icons/icon.ico'
var tag = 'notification-tag';
// Wait until payload is fetched
event.waitUntil(
self.registration.showNotification(title, {
body: body,
icon: icon,
tag: tag,
data: {} // Keeping this here in case I need it later
})
);
} else {
console.log("Event does not have data...");
}
}); // End push listener
// Notification Click event
self.addEventListener('notificationclick', function(event) {
console.log("Notification Clicked");
}); // End click listener
Personally, I will be creating a "generic" notification in case my data is funky, and will also be using try/catch. I suggest doing the same.
Follow these steps to achieve this:
In the browser:
You need to get the subscription object and save it, so your server has access to it: Read more about it
navigator.serviceWorker.ready.then(serviceWorkerRegistration => {
serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true})
.then(subscription => {
//save subscription.toJSON() object to your server
})});
In the server:
install web-push npm package
And send a web push like this:
const webpush = require('web-push');
setImmediate(async () => {
const params = {
payload: {title: 'Hey', body: 'Hello World'}
};
//this is the subscription object you should get in the browser. This is a demo of how it should look like
const subscription = {"endpoint":"https://android.googleapis.com/gcm/send/deC24xZL8z4:APA91bE9ZWs2KvLdo71NGYvBHGX6ZO4FFIQCppMsZhiTXtM1S2SlAqoOPNxzLlPye4ieL2ulzzSvPue-dGFBszDcFbSkfb_VhleiJgXRA8UwgLn5Z20_77WroZ1LofWQ22g6bpIGmg2JwYAqjeca_gzrZi3XUpcWHfw","expirationTime":null,"keys":{"p256dh":"BG55fZ3zZq7Cd20vVouPXeVic9-3pa7RhcR5g3kRb13MyJyghTY86IO_IToVKdBmk_2kA9znmbqvd0-o8U1FfA3M","auth":"1gNTE1wddcuF3FUPryGTZOA"}};
if (subscription.keys) {
params.userPublicKey = subscription.keys.p256dh;
params.userAuth = subscription.keys.auth;
}
// this key you should take from firebase console for example
// settings -> cloud messaging -> Server key
webpush.setGCMAPIKey('AAAASwYmslc:APfA91bGy3tdKvuq90eOvz4AoUm6uPtbqZktZ9dAnElrlH4gglUiuvereTJJWxz8_dANEQciX9legijnJrxvlapI84bno4icD2D0cdVX3_XBOuW3aWrpoqsoxLDTdth86CjkDD4JhqRzxV7RrDXQZd_sZAOpC6f32nbA');
try {
const r = await webpush.sendNotification(subscription, JSON.stringify(params));
console.log(r);
}
catch (e) {
console.error(e);
}
});