Only Receiving Part of Apple Subscription Notification in Google Cloud Function - ios

I am trying to set up a Google Cloud Function (GCF) to handle Subscription Notifications from Apple. I am familiar with GCF, but not very familiar with writing my own REST API and the Nodejs methods of handling the data Apple sends with the notification. I am receiving the Apple notification, but only a "chunk" of it. Here's my code (using express and body-parser frameworks). I put my whole function here to help people since there is absolutely nothing about how to use GCF for Subscription Notifications anywhere I could find on the web (note this code is very much a work in progress and I am new to Nodejs):
// Set up express object
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
exports.iosNotification = functions.https.onRequest((req, res) => {
console.log("We are receiving a request from Apple.");
app.use(bodyParser.json());
let receipt = req.body.latest_receipt;
console.log(req.body);
const chunks = [];
req.on('data', chunk => {
chunks.push(chunk);
console.log('A chunk of data has arrived:', chunk);
});
req.on('end', () => {
const data = Buffer.concat(chunks);
console.log('Data: ', data);
console.log('No more data');
});
const type = req.body.notification_type;
console.log("Notification type: ", type);
const lri = req.body.latest_receipt_info;
console.log(lri, receipt);
// Verify the receipt.
validateAppleReceipt(receipt)
.then((appleResponse) => {
console.log("Receipt from App Store server validated.", appleResponse);
res.sendStatus(200);
const oTxId = appleResponse.latest_receipt_info[0].original_transaction_id;
// Receipt is valid and we let Apple know. Let's process the notification.
switch (type) {
case 'CANCEL':
// User canceled the subscription (like on accidental purchase).
console.log("User canceled a subscription.");
break;
case 'DID_CHANGE_RENEWAL_PREF':
console.log("The subscriber downgraded. Effective on next renewal. Handle.");
break;
case 'DID_CHANGE_RENEWAL_STATUS':
console.log("The subscriber downgraded or upgraded. Effective on next renewal. Handle.");
break;
case 'DID_FAIL_TO_RENEW':
console.log("Subscription has a billing issue. Check if in billing retry period.");
break;
case 'DID_RECOVER':
console.log("Renewal of expired subscription that failed to renew.");
break;
case 'INITIAL_BUY':
console.log("Initial purchase. Ignored because we already handled with another function.");
break;
case 'INTERACTIVE_RENEWAL':
console.log("Interactive renewal. Not sure if we'll ever see this.");
break;
case 'RENEWAL':
console.log("Renewal after failure. Same as DID_RECOVER. Handle there.");
break;
default:
console.log("Hit default.");
break;
};
})
.catch((error) => {
console.log("Error validating receipt from App Store server.", error);
});
});
This is the output I'm getting (which is only a portion of the notification Apple says it is sending). I don't get notification_type or any of the other parts of the JSON file the Apple docs say I should be receiving:
{ latest_receipt: 'ewoJInNpZ25hdHVyZSIgPSAiQTNVM0FjaDJpbXRPMG53cEtrQW9 <<shortened for this post>>
I never see the console.log for any chunks.
What can I do to make sure I receive all the "chunks" and put them together into the complete JSON file that Apple is sending me so I can work with it?

I solved it. It was so simple. I just had to use req.body instead of req. Here's the code for anyone who is trying to use Google Cloud Functions to handle Server to Server Notifications for subscriptions from Apple.
exports.iosNotification = functions.https.onRequest((req, res) => {
console.log("We are receiving a request from Apple.");
let receipt = req.body.latest_receipt;
const type = req.body.notification_type;
console.log("Notification type: ", type);
const lri = req.body.latest_receipt_info;
console.log(type, lri, receipt);
// Verify the receipt.
validateAppleReceipt(receipt)
See code above for how to handle the types Apple sends...

Related

Flutter ios appstore validateReceipt on non-consumable in-app purchase

I seem to be stuck on this. Trying to validate the receipt (server side) on an in-app purchase on IOS (haven't tried with android, yet.)
I'm using the official in_app_purchase pub package.
This is the setup to initialize the purchase:
Future<bool> initiatePurchase() async {
...
(verify store is available)
..
print ("==> Store available, initiating purchase");
final PurchaseParam purchaseParam =
PurchaseParam(productDetails: _productDetails![0]);
await InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam);
return true;
}
Here's my verify purchase call:
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
PurchaseVerifRest purchaseRest = PurchaseVerifRest();
Map<String,dynamic> rsp = await purchaseRest.verifyPurchase(
{
"source": purchaseDetails.verificationData.source,
"vfdata": purchaseDetails.verificationData.serverVerificationData
});
// bundle up the source and verificationData in a map and send to rest
// call
return rsp['status'] == 200;
}
On the server side, the code looks like this (NodeJS/express app)
// (in router.post() call - 'purchaseData' is the map sent in the above code,
// the 'vfdata' member is the 'serverVerificationData'
//. in the 'purchaseDetails' object)
if (purchaseData.source == ('app_store')) {
const IOS_SHARED_SECRET = process.env...;
let postData = {
'receipt-data': purchaseData['vfdata'],
'password': IOS_SHARED_SECRET
};
try {
let verif_rsp = await execPost(postData);
retStatus = verif_rsp.statusCode;
msg = verif_rsp.data;
} catch (e) {
retStatus = e.statusCode;
}
}
What I get back, invariably is
210003 - Receipt could not be authenticated
... even though the purchase seems to go through, whether I validate or not.
Details/questions:
Testing with a sandbox account.
This is for a 'non-consumable' product purchase.
I'm assuming that purchaseDetails.verificationData.serverVerificationData is the payload containing the receipt to send to Apple for verification. Is this not correct? Is there another step I need to do to get the receipt data?
I've read in other posts that the verification step is only for recurring subscriptions and not for other types of products. Is this correct? I don't see anything in Apple's docs to indicate this.
Any thoughts appreciated.

How can I (using Twilio Functions example keyword handler) send a POST request to an external website when a user makes a specific selection?

When responding to user messages to a Twilio function, if they send a 6 digit set of numbers I want to send those 6 digits as well as the phone that sent them on to another website via POST request for authentication.
I am pretty new to this type of integration but have only been able to reply to the text message sender instead of sending a request to an external website.
/*
After you have deployed your Function, head to your phone number and configure the inbound SMS handler to this Function
*/
exports.handler = function(context, event, callback) {
let twiml = new Twilio.twiml.MessagingResponse();
const body = event.Body ? event.Body.toLowerCase() : null;
switch (body) {
case 'authcode':
twiml.message("Authenticate your account phone number to app.machinesaver.io by respond to this number with the 6-digit AuthCode sent when you signed up for an account at app.machinesaver.io/create-admin-account.");
break;
case 'help':
twiml.message("You can ask me ABOUT, ADDRESS, PHONE, or EMAIL");
break;
case 'about':
twiml.message("---------- is a technology company located in -------, -- USA.");
break;
case 'address':
twiml.message("Our address is: Address");
break;
case 'email':
twiml.message("Sales: \nService: \nAccounting: \n");
break;
case 'phone':
twiml.message("Main: PhoneNumber");
break;
default:
twiml.message("Sorry, I only understand HELP, ABOUT, ADDRESS, PHONE, AUTHCODE and EMAIL");
break;
}
callback(null, twiml);
};
There are no errors but I'm not sure what I can do next to send a POST request to a non Twilio URL from this function for just one of the responses. I imagine I can use a regular expression to find the case when a 6 digit number is texted but after that I am not sure how to send the POST request and wait for a response from the other website (basically authenticating this user's phone number by the 6 digit authcode).
The Twilio documentation recommends adding the following to do a POST request but I'm not sure where to add this in the handler to properly send the request and receive the response (perhaps after the case? or do I need to declare the variables at the top and put the rest in after the case for an AUTHCODE?):
var got = require('got');
var requestPayload = {foo: 'bar'};
got.post('https://your-api.com/endpoint',
{ body: JSON.stringify(requestPayload),
headers: {
'accept': 'application/json'
},
json: true
}).then(function(response) {
console.log(response.body)
callback(null, response.body);
}).catch(function(error) {
callback(error)
});
There are a couple of approaches:
Twilio functions run on nodejs, so you could use native node modules such as HTTP to make a POST request and wait for the response synchronously.
You could pass control of the message response outside of the twilio function using Redirect . Using the redirect twiml will require your external function to return valid twiml.
Twilio developer evangelist here.
Twilio Functions are just running Node.js, so you can make HTTP requests with the native HTTP module (as Aaron suggests) or by adding dependencies (like got) in the config section of your Twilio console and using them.
The important thing about doing asynchronous actions, like making an HTTP request, in a Twilio Function is to call the callback function when the action has completed, otherwise the action may get cut off before it completes.
Taking your example and expanding on it, sending an optional HTTP request might look a bit like this:
const got = require('got');
exports.handler = function(context, event, callback) {
let twiml = new Twilio.twiml.MessagingResponse();
const body = event.Body ? event.Body.toLowerCase() : null;
switch (body) {
case 'authcode':
twiml.message("Authenticate your account phone number to app.machinesaver.io by respond to this number with the 6-digit AuthCode sent when you signed up for an account at app.machinesaver.io/create-admin-account.");
break;
case 'help':
twiml.message("You can ask me ABOUT, ADDRESS, PHONE, or EMAIL");
break;
case 'about':
twiml.message("---------- is a technology company located in -------, -- USA.");
break;
case 'address':
twiml.message("Our address is: Address");
break;
case 'email':
twiml.message("Sales: \nService: \nAccounting: \n");
break;
case 'phone':
twiml.message("Main: PhoneNumber");
break;
default:
twiml.message("Sorry, I only understand HELP, ABOUT, ADDRESS, PHONE, AUTHCODE and EMAIL");
break;
}
if (// some condition that triggers the HTTP request) {
got.post(url, {
body: JSON.stringify({ example: 'parameters' }),
json: true
}).then(response => {
callback(null, twiml);
}).catch(error => {
callback(error);
});
} else {
callback(null, twiml);
}
};
Let me know if that helps at all.

How to dispatch a Paypal IPN to a Google Cloud function?

I've read here that it's possible to send an IPN directly to a Google cloud function. I have my Google Cloud functions running on Firebase on an index.js file.
I've set up my Paypal buttons to send the IPN to a page on my webapp.
Here is an example of one of the functions I'm running off Google Cloud Functions/Firebase:
// UPDATE ROOMS INS/OUTS
exports.updateRoomIns = functions.database.ref('/doors/{MACaddress}').onWrite((change, context) => {
const beforeData = change.before.val();
const afterData = change.after.val();
const roomPushKey = afterData.inRoom;
const insbefore = beforeData.ins;
const insafter = afterData.ins;
if ((insbefore === null || insbefore === undefined) && (insafter === null || insafter === undefined) || insbefore === insafter) {
return 0;
} else {
const updates = {};
Object.keys(insafter).forEach(key => {
updates['/rooms/' + roomPushKey + '/ins/' + key] = true;
});
return admin.database().ref().update(updates); // do the update}
}
return 0;
});
Now question:
1) I want to add another function to process IPN from Paypal as soon as I have a transaction. How would I go about this?
I'll mark the answer as correct if solves this first question.
2) how would that Google cloud function even look like?
I'll create another question if you can solve this one.
Note I am using Firebase (no other databases nor PHP).
IPN is simply a server that tries to reach a given endpoint.
First, you have to make sure that your firebase plan supports 3rd party requests (it's unavailable in the free plan).
After that, you need to make an http endpoint, like so:
exports.ipn = functions.http.onRequest((req, res) => {
// req and res are instances of req and res of Express.js
// You can validate the request and update your database accordingly.
});
It will be available in https://www.YOUR-FIREBASE-DOMAIN.com/ipn
Based on #Eliya Cohen answer:
on your firebase functions create a function such as:
exports.ipn = functions.https.onRequest((req, res) => {
var reqBody = req.body;
console.log(reqBody);
// do something else with the req.body i.e: updating a firebase node with some of that info
res.sendStatus(200);
});
When you deploy your functions go to your firebase console project and check your functions. You should have something like this:
Copy that url, go to paypal, edit the button that's triggering the purchase, scroll down to Step 3 and at the bottom type:
notify_url= paste that url here
Save changes.
You can now test your button and check the req.body on your firebase cloud functions Log tab.
Thanks to the answers here, and especially to this gist: https://gist.github.com/dsternlicht/fdef0c57f2f2561f2c6c477f81fa348e,
.. finally worked out a solution to verify the IPN request in a cloud func:
let CONFIRM_URL_SANDBOX = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr';
exports.ipn = functions.https.onRequest((req, res) => {
let body = req.body;
logr.debug('body: ' + StringUtil.toStr(body));
let postreq = 'cmd=_notify-validate';
// Iterate the original request payload object
// and prepend its keys and values to the post string
Object.keys(body).map((key) => {
postreq = `${postreq}&${key}=${body[key]}`;
return key;
});
let request = require('request');
let options = {
method: 'POST',
uri : CONFIRM_URL_SANDBOX,
headers: {
'Content-Length': postreq.length,
},
encoding: 'utf-8',
body: postreq
};
res.sendStatus(200);
return new Promise((resolve, reject) => {
// Make a post request to PayPal
return request(options, (error, response, resBody) => {
if (error || response.statusCode !== 200) {
reject(new Error(error));
return;
}
let bodyResult = resBody.substring(0, 8);
logr.debug('bodyResult: ' + bodyResult);
// Validate the response from PayPal and resolve / reject the promise.
if (resBody.substring(0, 8) === 'VERIFIED') {
return resolve(true);
} else if (resBody.substring(0, 7) === 'INVALID') {
return reject(new Error('IPN Message is invalid.'));
} else {
return reject(new Error('Unexpected response body.'));
}
});
});
});
Also thanks to:
https://developer.paypal.com/docs/classic/ipn/ht-ipn/#do-it
IPN listener request-response flow: https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNImplementation/
To receive IPN message data from PayPal, your listener must follow this request-response flow:
Your listener listens for the HTTPS POST IPN messages that PayPal sends with each event.
After receiving the IPN message from PayPal, your listener returns an empty HTTP 200 response to PayPal. Otherwise, PayPal resends the IPN message.
Your listener sends the complete message back to PayPal using HTTPS POST.
Prefix the returned message with the cmd=_notify-validate variable, but do not change the message fields, the order of the fields, or the character encoding from the original message.
Extremely late to the party but for anyone still looking for this, PayPal have made a sample in their JS folder on their IPN samples Github repo.
You can find this at:
https://github.com/paypal/ipn-code-samples/blob/master/javascript/googlecloudfunctions.js

Firebase Cloud functions - collapsible IOS notifications

I use cloud functions to send notifications. It works, but I want collapsible messages. How can I do it? Here is my current function:
exports.sendNotifications = functions.database
.ref("/users/{userId}/data")
.onWrite(event => {
const userId = event.params.userId;
if (!event.data.val()) {
return;
}
const payload = {
notification: {
title: `Hey`,
body: 'It's your turn!'
//icon: receiver.photoURL
}
};
const options = {
collapseKey: 'myturnkey'
};
return admin
.database()
.ref(`users\/${userId}\/data\/notificationkey`)
.once('value')
.then(data => {
console.log('inside key', data.val());
if (data.val()) {
return admin.messaging().sendToDevice(data.val(), payload, options);
}
});
});
I tried "collapseKey" and "collapse_key" in options but none of them works, I still receive notifications every time my function is called, so I get a list of notifications on my iphone whereas I want only one.
EDIT
I also tried the parameter "apns-collapse-id" according to the FCM messages documentation, but when when I try to deploy the functions the console says " apns-collapse-id: 'myturnkey', ^ SyntaxError: Unexpected token -"
Thank you,
Alexandre
You can do this, or manipulate current notifications in other ways, using the registration.getNotifications() API which gives you access to all the currently visible notifications for your web app.
But you need to write this code in your Service-worker
see this documention for merging-notifications

Send notification from web to android device using Firebase

I am trying for a while now to implement this flow: When user adds some files on server app, notification should trigger and send from server to FCM and that from there to pass message saying something like: 'New file has been added'.
Basically I want to inform mobile device user that something on server has been changed.
I have tried many things, but nothing seems to work as I would expect, at least.
On the mobile side I have set up Firebase inside my Xamarin.Android project, and when I am sending notifications directly from Firebase console, I get notifications, and everything is good.
But I don't want to send notifications via Firebase console, I would rather send notification from server (which is ASP.NET MVC project) to Firebase console and then pass it from there to android device.
My first question would be: Has anybody got an idea how can I inform web app about device_id? Is there some way that android device send this information on server? And maybe from there I can store that data and update it occasionally, since it is basically a refresh token.
My second problem is this: Even when I hard code current device_id of an active android device and try to send a message from server whit this code:
public class FirebaseService : IFirebaseService
{
public void SendMessageToClientApplication(string message, string serverApiKey, string senderId, string deviceId)
{
AndroidFCMPushNotificationStatus result = new AndroidFCMPushNotificationStatus();
try
{
result.Successful = false;
result.Error = null;
deviceId = "eMk6mD8P8Dc:APA91bG5Lmqn4Hwb4RZJ1Mkdl8Rf_uYQsQCEfDJK334tzSvIGzdao7o2X6VmtcTEp_Li0mG8iUoUT7-_RnZxQKocHosZwx6ITWdpmQyCwUv60IIIy0vxNlEaccT6RqK6c-cE1C6I3FTT";
var value = message;
WebRequest tRequest = WebRequest.Create("https://fcm.googleapis.com/fcm/send");
tRequest.Method = "post";
tRequest.ContentType = "application/x-www-form-urlencoded;charset=UTF-8";
tRequest.Headers.Add(string.Format("Authorization: key={0}", serverApiKey));
tRequest.Headers.Add(string.Format("Sender: id={0}", senderId));
string postData = "collapse_key=score_update&time_to_live=108&delay_while_idle=1&data.message="
+ value + "&data.time=" + DateTime.Now.ToString() + "&registration_id=" + deviceId + "";
Byte[] byteArray = Encoding.UTF8.GetBytes(postData);
tRequest.ContentLength = byteArray.Length;
using (Stream dataStream = tRequest.GetRequestStream())
{
dataStream.Write(byteArray, 0, byteArray.Length);
using (WebResponse tResponse = tRequest.GetResponse())
{
using (Stream dataStreamResponse = tResponse.GetResponseStream())
{
using (StreamReader tReader = new StreamReader(dataStreamResponse))
{
String sResponseFromServer = tReader.ReadToEnd();
result.Response = sResponseFromServer;
}
}
}
}
}
catch (Exception ex)
{
result.Successful = false;
result.Response = null;
result.Error = ex;
}
}
}
I get nothing both in Firebase console and of course nothing on device as well.
I have tried to implement Firebase web as javascript on my server app like this:
<script>
var config = {
apiKey: "mykey",
authDomain: "myauthdomain",
databaseURL: "mydatabaseurl",
projectId: "myprojectid",
storageBucket: "mystoragebucket",
messagingSenderId: "mysenderid"
};
window.onload = function () {
firebase.initializeApp(config);
const messaging = firebase.messaging();
messaging.requestPermission()
.then(function () {
console.log('Notification permission granted.');
return messaging.getToken()
})
.then(function (token) {
console.log(token);
})
.catch(function (err) {
console.log('Unable to get permission to notify.', err);
});
messaging.onMessage(function (payload) {
console.log('onMessage: ', payload);
});
}
</script>
But this code gets some kind of a different device_id(aka token), probably one generated for that server machine.
Does anybody has experience with sending device_id to server app and from there sending notification message to Firebase console? I would appreciate some code examples, tutorials or anything that can help, since I was unable to find something useful during my google search.
My first question would be: Has anybody got an idea how can I inform web app about device_id?
The most common approach is to store the list of device tokens (each device that uses FCM has such a token) in a database, such as the Firebase Database. There is an example of this in the Cloud Functions for Firebase documentation. In this example the devices receiving the messages are web pages, but the approach is the same for iOS and Android.
I also recommend reading Sending notifications between Android devices with Firebase Database and Cloud Messaging. In this article, instead of sending to a device token, each user subscribes to a topic. That prevents having to manage the device tokens in your code.

Resources