How to capture paypal webhook? - paypal

I've integrated PayPal Smart Buttons in my website, both createOrder and Capture are processed on the server side, when a payment has been completed the transation is available on business sandbox account and the webhook event is registered in Webhooks Events page.
The webhook POST url is subscribed in dashboard to all event and to test it in localhost i use webhookrelay.
The issue is that i get the webhook called only when the event comes from the Webhook Simulator and not from an actual payment from Smart Button.
So my server receives webhook calls only from the simulator and is NOT triggered from a Smart Button payment.
I'm in sandbox mode and all payments and Smart Buttons are in sandbox mode.
Here is my Smart Button code:
paypal
.Buttons({
style: {
shape: "rect",
color: "gold",
layout: "horizontal",
label: "paypal",
tagline: false,
height: 52,
},
createOrder: async function () {
const res = await fetch(
"https://www.example.it/payment/paypal/order/create/" + orderID,
{
method: "post",
headers: {
"content-type": "application/json",
},
credentials: "include",
}
);
const data = await res.json();
return data.id;
},
onApprove: async function (data) {
const res = await fetch(
"https://www.example.it/payment/paypal/" +
data.orderID +
"/capture/",
{
method: "post",
headers: {
"content-type": "application/json",
},
credentials: "include",
}
);
const details = await res.json();
if (localStorage.STBOrder) {
localStorage.removeItem("STBOrder");
}
$("#modalPayments").modal("hide");
$("#modalSuccess").modal("show");
},
onCancel: function (data) {},
})
.render("#paypal-button-container");

Since you are capturing on the server side, there is no real motivation to use webhooks. The response of your API call to do the capture is already authoritative; a webhook provides no useful additional information
If you insist on implementing webhooks for some reason, you need to register a listener for the events you want to receive. There is an API for managing webhook registrations: https://developer.paypal.com/docs/api/webhooks/v1/#webhooks_post , or you can try the steps in this guide https://developer.paypal.com/docs/api-basics/notifications/webhooks/rest/#subscribe-to-events
Again, implementing asynchronous webhooks is completely unnecessary when you have already implemented a synchronous capture API call on your server.

Related

POST data to Google Sheet web app from AWS Lambda

CURRENTLY
I have a Google Sheets App Script 'web app'
Script in Goolge Sheets
function doPost(e) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Sheet1");
sheet.getRange("A1").setValue("Hello!")
return "Success!"
}
Google Apps Script Web App Config:
Execute as: Me // or as User. I've tried both.
Who has access: Anyone within MyOrganisation
I want to make a POST request to the above Web App from AWS Lambda.
AWS Lambda .js:
const { GoogleSpreadsheet } = require("google-spreadsheet");
const doc = new GoogleSpreadsheet(
{spreadsheetId}
);
await doc.useServiceAccountAuth({
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n"),
});
let token = doc["jwtClient"]["credentials"]["access_token"];
await new Promise((resolve, reject) => {
const options = {
host: 'script.google.com',
path: "/macros/s/{myscriptid}/exec", //<-- my web app path!
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': "Bearer "+ token
}
};
//create the request object with the callback with the result
const req = HTTPS.request(options, (res) => {
resolve(JSON.stringify(res.statusCode));
});
// handle the possible errors
req.on('error', (e) => {
reject(e.message);
});
//do the request
req.write(JSON.stringify(data));
//finish the request
req.end();
});
console.log("response:"+JSON.stringify(response))
GCP Service Account
I have a GCP Service Account, with permission to Google Sheets API, and otherwise unrestricted access.
This Service account has EDIT access to the Google Sheet with the doPost(e) script.
Token Output:
"jwtClient": {
"_events": {},
"_eventsCount": 0,
"transporter": {},
"credentials": {
"access_token": "somelongvalue...............", //<-- what I use
"token_type": "Bearer",
"expiry_date": 1661662492000,
"refresh_token": "jwt-placeholder"
},
"certificateCache": {},
"certificateExpiry": null,
"certificateCacheFormat": "PEM",
"refreshTokenPromises": {},
"eagerRefreshThresholdMillis": 300000,
"forceRefreshOnFailure": false,
"email": "serviceaccount#appspot.gserviceaccount.com",
"key": "-----BEGIN PRIVATE KEY-----\nsomelongvalue=\n-----END PRIVATE KEY-----\n",
"scopes": [
"https://www.googleapis.com/auth/spreadsheets"
],
"subject": null,
"gtoken": {
"key": "-----BEGIN PRIVATE KEY-----\nsomelongvalue=\n-----END PRIVATE KEY-----\n",
"rawToken": {
"access_token": "somelongvalue...............",
"expires_in": 3599,
"token_type": "Bearer"
},
"iss": "serviceaccount#appspot.gserviceaccount.com",
"sub": null,
"scope": "https://www.googleapis.com/auth/spreadsheets",
"expiresAt": 1661662492000
}
}
ISSUE
Current response:
response:"401"
I cannot find any Google documentation on how to setup the headers to authenticate a request (from my service account) to my organisation restricted web app.
When the Web App is open to "Anyone" then it runs fine, but as soon as I restrict to MyOrganisation, I struggle to find a way to authenticate my POST request.
HELP!
How do I set up a POST request to my Google Sheets web app such that it can be protected by authentication? Right now, I'd be happy to find ANY means to authenticate this request (not necessarily a service account) that doesn't leave it completed open to public.
Should I use this hack?
One idea I had was to put a "secret" into my lambda function, and then make the web app public. The web app would check the secret, if if matched, would execute the function.
Modification points:
In order to access Web Apps using the access token with a script, the scopes of Drive API are required to be included. Those are https://www.googleapis.com/auth/drive.readonly, https://www.googleapis.com/auth/drive, and so on. Ref
When I saw your showing script, it seems that the access token is retrieved using google-spreadsheet. When I saw the script of google-spreadsheet, it seems that this uses only the scope of https://www.googleapis.com/auth/spreadsheets. Ref
From this situation, I thought that the reason for your current issue might be due to this. If my understanding is correct, how about the following modification? In this modification, the access token is retrieved by googleapis for Node.js from the service account. Ref
Modified script:
Google Apps Script side:
function doPost(e) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Sheet1");
sheet.getRange("A1").setValue("Hello!")
return ContentService.createTextOutput("Success!"); // Modified
}
When you modified the Google Apps Script, please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful about this.
You can see the detail of this in the report "Redeploying Web Apps without Changing URL of Web Apps for new IDE".
Node.js side:
const { google } = require("googleapis");
const HTTPS = require("https");
const auth = new google.auth.JWT(
"###", // Please set client_email here.
null,
"###", // Please set private_key here. When you set private_key of service account, please include \n.
["https://www.googleapis.com/auth/drive.readonly"],
null
);
function req(token) {
return new Promise((resolve, reject) => {
const data = { key1: "value1" }; // Please set your value.
const options = {
host: "script.google.com",
path: "/macros/s/{myscriptid}/exec", //<-- my web app path!
method: "POST",
headers: {Authorization: "Bearer " + token},
};
const req = HTTPS.request(options, (res) => {
if (res.statusCode == 302) {
HTTPS.get(res.headers.location, (res) => {
if (res.statusCode == 200) {
res.setEncoding("utf8");
res.on("data", (r) => resolve(r));
}
});
} else {
res.setEncoding("utf8");
res.on("data", (r) => resolve(r));
}
});
req.on("error", (e) => reject(e.message));
req.write(JSON.stringify(data));
req.end();
});
}
auth.getAccessToken().then(({ token }) => {
req(token).then((e) => console.log(e)).catch((e) => console.log(e));
});
When this script is run, when the Web Apps is correctly deployed, the script of Web Apps is run and Success! is returned.
Note:
If this modified script was not useful for your Web Apps setting, please test as follows.
Please confirm whether your service account can access to the Spreadsheet again.
Please share the email address of the service account on the Spreadsheet. From your showing Google Apps Script, I thought that your Google Apps Script is the container-bound script of the Spreadsheet.
Please reflect the latest script to the Web Apps.
When you modified the Google Apps Script, please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful about this.
You can see the detail of this in the report "Redeploying Web Apps without Changing URL of Web Apps for new IDE".
When you set private_key of service account, please include \n.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script
Added:
When you will directly put the value to the Spreadsheet using Sheets API with google-spreadsheet module, you can also use the following script.
const { GoogleSpreadsheet } = require("google-spreadsheet");
const sample = async () => {
const doc = new GoogleSpreadsheet("###"); // Please set your Spreadsheet ID.
await doc.useServiceAccountAuth({
client_email: client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY,
});
await doc.loadInfo();
const sheet = doc.sheetsByTitle["Sheet1"];
await sheet.loadCells("A1");
sheet.getCell(0, 0).value = "Hello!";
await sheet.saveUpdatedCells();
};
sample();
In this case, your service account is required to be able to access to the Spreadsheet. Please be careful about this.

PayPal integration with PHP: which API and credentials to use?

I am trying to integrate paypal api with PHP compared to the old button form that I have used to date on my sites. But there is one thing that is not clear to me, is it more correct to integrate paypal with client_id and secret or through the codes provided in the account panel (api username, api password and signature)? I followed the REST API integration guide (version 2) but they require client_id and secret. So what is the data in the account panel for? Anyone can clarify my ideas? Thank you
The API username, password, and signature is used by the classic NVP/SOAP APIs, which are much older than the REST API. They exist only for backwards compatibility with old shopping cart software and such integrations.
The v2/checkout/orders API should be used for current PayPal Checkout integrations. Pair two routes on your server (one for create order, one for capture order) that return/output only JSON data (never any HTML or text) with this JS approval flow.
I would go with JS SDK inline integration - requires client-id only and is more flexible than checkout buttons. Also creates nice user experience as if staying on the page (no redirects to 3rd party site). See all demos here.
paypal.Buttons({
createOrder: function(data, actions) {
return actions.order.create({
// note: custom_id (127 chars) will be passed later back in capture
purchase_units: [{
amount: {
value: amtDue.toFixed(2),
currency: 'EUR'
},
description : description,
custom_id : '<?= $encryptedPaymentData ?>'
}]
});
},
onApprove: function(data, actions) {
$("#global-spinner").show();
// set custom capture handler
return actions.order.capture().then(function(details) {
$.ajax({
type: "POST",
url: "/paypal/success",
data: ({
details : details,
ad : amtDue,
desc : description,
_token : '<?= $csrf ?>'
}),
success: function(resp) {
$("#global-spinner").hide();
window.showThankYou(); // some "thank you" function
},
error: function() {
$("#global-spinner").hide();
alert("Connection error.");
}
});
});
},
onError: function (err) {
// some custom function - send error data to server logger
window.handlePaypalError(err, description, amtDue);
}
}).render('#paypal-button-container');

Service workers "sync" operation is working while its offline?

I have a PWA project where I send the data to server. During this process, if the user is offline then the data is stored in indexedDb and a sync tag is registered. So, then when the user comes online that data can sent to the server.
But In my case the sync event gets executed immediately when the we register a sync event tag, which means the data is tried to be sent to server while its offline, which is not going to work.
I think the sync event supposed to fire while its online only, what could be issue here ?
The service worker's sync event works accordingly when I tried to enable and disable the offline option of chrome devtools, and also works correctly in my android phone.
This is how I register my sync tag
function onFailure() {
var form = document.querySelector("form");
//Register the sync on post form error
if ('serviceWorker' in navigator && 'SyncManager' in window) {
navigator.serviceWorker.ready
.then(function (sw) {
var post = {
datetime1: form.datetime1.value,
datetime: form.datetime.value,
name: form.name.value,
image: form.url.value,
message: form.comment.value
};
writeData('sync-comments', post)
.then(function () {
return sw.sync.register('sync-new-comment');
})
.then(function () {
console.log("[Sync tag registered]");
})
.catch(function (err) {
console.log(err);
});
});
}
}
And this is how the sync event is called
self.addEventListener('sync', function (event) {
console.log("[Service worker] Sync new comment", event);
if (event.tag === 'sync-new-comment') {
event.waitUntil(
readAllData('sync-comments')
.then(function (data) {
setTimeout(() => {
data.forEach(async (dt) => {
const url = "/api/post_data/post_new_comment";
const parameters = {
method: 'POST',
headers: {
'Content-Type': "application/json",
'Accept': 'application/json'
},
body: JSON.stringify({
datetime: dt.datetime,
name: dt.name,
url: dt.image,
comment: dt.message,
datetime1: dt.datetime1,
})
};
fetch(url, parameters)
.then((res) => {
return res.json();
})
.then(response => {
if (response && response.datetimeid) deleteItemFromData('sync-comments', response.datetimeid);
}).catch((error) => {
console.log('[error post message]', error.message);
})
})
}, 5000);
})
);
}
});
you mention
The service worker's sync event works accordingly when I tried to enable and disable the offline option of chrome devtools, and also works correctly in my android phone.
So I'm not sure which case is the one failing.
You are right that the sync will be triggered when the browser thinks the user is online, if the browser detects that the user is online at the time of the sync registration it will trigger the sync:
In true extensible web style, this is a low level feature that gives you the freedom to do what you need. You ask for an event to be fired when the user has connectivity, which is immediate if the user already has connectivity. Then, you listen for that event and do whatever you need to do.
Also, from the workbox documentation
Browsers that support the BackgroundSync API will automatically replay failed requests on your behalf at an interval managed by the browser, likely using exponential backoff between replay attempts.

403 on Google Action push notification request

I'm trying to perform a push notification for Google Actions Intent.
Thus far, I've followed the instructions here: https://developers.google.com/actions/assistant/updates/notifications#send_notifications
This is my resulting code:
const {google} = require('googleapis');
var request = require('request');
const key = require('./bot.json');
module.exports = async function (context, myQueueItem) {
context.log('JavaScript queue trigger function processed work item', myQueueItem);
let jwtClient = new google.auth.JWT(
key.client_email, null, key.private_key,
['https://www.googleapis.com/auth/actions.fulfillment.conversation'],
null
);
jwtClient.authorize((err, tokens) => {
// code to retrieve target userId and intent
let notif = {
userNotification: {
title: [message],
},
target: {
userId:[obtained from permission request],
intent: [name of intent],
// Expects a IETF BCP-47 language code (i.e. en-US)
locale: 'en-US'
},
};
request.post('https://actions.googleapis.com/v2/conversations:send', {
'auth': {
'bearer': tokens.access_token,
},
'json': true,
'body': {'customPushMessage': notif},
}, (err, httpResponse, body) => {
console.log(body);
console.log(httpResponse.statusCode + ': ' + httpResponse.statusMessage);
});
});
};
//module.exports(console, "Test");
This results in a 403 from the notification service. Is this because of the user id, intent name or jwtoken that was generated?
Following are the steps we need to check before sending the push notification
Check your Google permission settings
In order to test the Action, you need to enable the necessary permissions.
Go to the ‘Activity Controls' page (https://myaccount.google.com/activitycontrols).
Sign in with your Google account, if you have not already done so.
Ensure that the following permissions are enabled:
a.Web & App Activity
b.Device Information
c.Voice & Audio Activity
2.Target intent name should be added into the Implicit invocation field.
with enabled push notification.
3.use the same email id in your google assistant which you had used for login in GCP.

Paypal webhooks events are not triggered in sandbox

Hello I am using the following setup
Created paypal merchant and customer sandbox accounts
Configured paypal REST API app
Added a webhook url to my server and have validated that it works using the webhook simulator
Used the Express Checkout javascript implementation found here
I am able to make successful payments when viewing the notifications in sandbox but no webhook is ever triggered???
Below is a sample of my javascript implementation that I have used, please not that it's embedded in a coldfusion script file hence the use hashtags.
`
var items = #paypalItems#;
// Render the PayPal button
paypal.Button.render({
env: '#application.config.paypal.bSandbox ? "sandbox" : "production"#', // sandbox | production
commit: true,
//style the button
style: {
label: 'pay'
},
// PayPal Client IDs - replace with your own
client: {
sandbox: '#application.config.paypal.sandbox_key#',
production: '#application.config.paypal.live_key#'
},
// Wait for the PayPal button to be clicked
payment: function(data, actions) {
// Make a client-side call to the REST api to create the payment
return actions.payment.create({
payment: {
transactions: [{
amount: {
total: #trim(numberFormat( application.oCart.getTotal(bDiscount=1,bShipping=1) , "99999999.99" ))#,
currency: "AUD",
details: {
subtotal: #trim(numberFormat( application.oCart.getTotal() - application.oCart.getAmountGST( amount=application.oCart.getTotal(bDiscount=1), addGST=false ), "99999999.99" ))#,
tax: #trim(numberFormat(application.oCart.getAmountGST( amount=application.oCart.getTotal(bDiscount=1), addGST=false ), "99999999.99" ))#,
shipping: #trim(numberFormat( application.oCart.oShipping.getCartShippingAmount(country=session.fcbshoppingCart.order.shippingCountry), "99999999.99" ))#
}
},
invoice_number: "#orderNumber#",
item_list: {
items: items,
shipping_address: {
recipient_name: "#session.fcbshoppingCart.customer.firstName# #session.fcbshoppingCart.customer.lastName#",
line1: "#session.fcbshoppingCart.order.shippingAddress1#",
line2: "#session.fcbshoppingCart.order.shippingAddress2#",
city: "#session.fcbshoppingCart.order.shippingSuburb#",
country_code: "#paypalCountryCode#",
postal_code: "#session.fcbshoppingCart.order.shippingPostCode#",
state: "#session.fcbshoppingCart.order.shippingState#"
}
}
}]
}
});
},
// Wait for the payment to be authorized by the customer
onAuthorize: function(data, actions) {
console.log( "Paypal Authorize:", data );
// Execute the payment
return actions.payment.execute().then(function(payment) {
console.log( "Paypal Response:", payment );
//payment has been accepted so we can now generate an order
$.ajax({
type: "get",
url: "/apps/paypal/createOrder.cfm",
data: {
transactionNumber: "#orderNumber#",
payPalPaymentId: data.paymentID
},
dataType: "json",
success: function( res ) {
console.log('edharry create order data', res);
if( res.BPAYMENTPROCEED ) {
$('##paypal-message').addClass("show success").text('Payment Successfully Complete!');
//lets redirect to the checkout success page.
window.location.href = window.location.origin + '/shop/checkout/confirmation?productOrder=' + res.PRODUCTORDER.OBJECTID;
} else {
//need to handle a failed transaction
$('##paypal-message').addClass("show failure").text('Payment did not complete on server!');
}
},
error: function() {
//lets show an error
$('##paypal-message').addClass("show failure").text('Payment did not complete on server!');
}
})
$('##paypal-message').addClass("show success").text('Payment Successfully Complete!');
});
},
onCancel: function(data) {
console.log('The payment was cancelled!');
}
}, '##paypal-button-container');`
This is an ongoing issue with Paypal. They are aware of this issue and are currently working to resolve this.