Jasonette / JASONETTE-iOS

📡 Native App over HTTP, on iOS
https://www.jasonette.com
MIT License
5.26k stars 352 forks source link

Push notifications #7

Open elfeffe opened 8 years ago

elfeffe commented 8 years ago

Great project! What about push notifications?

gliechtenstein commented 8 years ago

Great question. Before I start explaining, please correct me if I'm wrong. I think I have enough experience with push notifications (Jasonette started out as something extracted out from a messaging app.

As far as I know, push notification in most cases is a server-side thing. Let me use a messaging app as an example:

  1. John signs up to the app. After the sign up is complete, the app sends the phone's device token to the app's server.
  2. The server stores the device token under John's account for later reference.
  3. Jane signs up to the same app. Jane finds John and sends a message.
  4. When Jane submits the message, it sends a POST request to the server.
  5. The server looks for John in the DB and creates a chat message in the DB.
  6. Then, the server looks up John's device token.
  7. The push notification on iPhones need to go through Apple's push server (APNS), so the app's server makes a POST request to APNS with John's device token.
  8. APNS delivers the push to the phone associated with the device token, which is John's phone.

Of course, there is a way to bypass this. For example if Jane's phone knows exactly what John's device token is, then it can directly jump to step 7 and send a push to that device token through APNS immediately. But in most cases this is not realistic since most push based apps need some sort of centralized way of storing and managing device tokens for their users.

That said, there are many ways to tackle this problem.

The project is not yet at a stage where I want to make a decision on whether I will build a dedicated push broker service (Option 3) or not. And honestly I think building Jasonette into something that's tightly coupled with a "special server" may limit its potential.

I think option 2 feels most rational at this point, since you still have a way to send device tokens to your server without touching any native code, but you're not tied to a specific push provider.

But again, let me know if you know a better solution. Otherwise I will soon start working on the 'option 2' and post an update here :)

seletz commented 7 years ago

An action for sending tokens and new system event which would be fired when a push message arrives would be cool.

I fully agree that writing yet another push service backend would bind jasonette too tightly to one implementation.

OTOH I see that both UA and MS Azure require hooking their own frameworks on the client (iOS) side. I think makes it very difficult to come up with a generic solution for a system event.

gliechtenstein commented 7 years ago

Here's the push notification branch: https://github.com/Jasonette/JASONETTE-iOS/compare/push_notification?expand=1

I've just tested it and it works on mine, but as is always with push notifications you need to jump through a couple of certificate hoops.

I'm assuming you have an apple developer account, since that's the only way to support push notifications. Anyway, to make this work, you should:

  1. Do all the push related certificate setup from developer.apple.com (like generating certificates and profile)
  2. Download this push notification branch
  3. Go to Capabilities tab in XCode and enable Push Notifications => This will create an entitlements file automatically
  4. Go to General tab in XCode and DISABLE "automatically manage signing" and manually set that to the production app profile you created from developer.apple.com. (I think this is a one of those bugs on Apple's end and have seen many other projects suffer from the same problem. If anyone knows how to get around this please share.)

Note that these are not Jasonette related steps but something anyone who wants to implement push notification on iOS needs to go through.


Once you're all set up, this is how the new API should work:

  1. $notification.register action => Registers this device to Apple for push notification
  2. Then Apple server returns a device token => this triggers $notification.registered event with $jason.device_token set as the newly assigned device token
  3. You can use this value to do whatever you want. In the following example we send it to our server. It will probably store the device_token under current user's database entry
  4. Also, when the user receives a push notification while the app is in the foreground, it triggers $notification.remote event. In this case the entire push payload will be stored inside $jason. you can utilize this value to run any other action. In this case we call a $util.banner.
     {
         "$jason": {
            "head": {
                "title": "PUSH TEST",
                "actions": {
                    "$load": {
                        "type": "$notification.register"
                    },
                    "$notification.registered": {
                        "type": "$network.request",
                        "options": {
                            "url": "https://myserver.com/register_device.json",
                            "data": {
                                "device_token": "{{$jason.device_token}}"
                            }
                        }
                    },
                    "$notification.remote": {
                        "type": "$util.banner",
                        "options": {
                            "title": "Message",
                            "description": "{{JSON.stringify($jason)}}"
                        }
                    }
                }
            }
         }
     }

One more thing, I'm attaching the server-side code (it's an AWS lambda function but you can do whatever you want) I used to get it to work just in case. It's all hard-coded because it's just for validating the API:


// This app downloads the p8.key file from my S3 everytime, you can try it with your own S3. But anyway, downloading push certificate everytime is needless overhead so please don't do this in production.

var AWS = require('aws-sdk'); 
var s3 = new AWS.S3(); 
var fs = require('fs');
var apn = require('apn');
var options = {
  token: {
    key: "/tmp/key.p8",
    keyId: "[[REDACTED]]",
    teamId: "[[REDACTED]]"
  },
  production: true
};

var run = function(payload, ctx){
  var apnProvider = new apn.Provider(options);
  var note = new apn.Notification();
  note.expiry = Math.floor(Date.now() / 1000) + 3600; // Expires 1 hour from now.
  note.sound = "pop.wav";
  note.badge = 3;
  note.alert = "\uD83D\uDCE7 \u2709 You have a new message";
  note.payload = {'messageFrom': 'Ethan Gliechtenstein'};
  note.topic = "[[YOUR APP BUNDLE ID]]";

  // Normally this device token should be dynamically looked up but I hard coded it by finding out what the device token is. I used some JSON markup like this to email myself the device token:
  // "actions": {"$notification.registered": {"type": "$href", "options": {"url": "mailto:ethan.gliechtenstein@gmail.com?subject={{$jason.device_token}}", "view": "app"}}}

  apnProvider.send(note, "[[HARDCODED DEVICE TOKEN]]").then( (result) => {
      // see documentation for an explanation of result
      console.log(JSON.stringify(result));
      ctx.succeed({ "response": "OK" })
  });
};

exports.handle = function(e, ctx) {
  // Need to download cert from S3 first
  var file = require('fs').createWriteStream('/tmp/key.p8');
  var key_dest = s3.getObject({Bucket: "jasonette", Key: "cert/key.p8"}).createReadStream();
  key_dest.on("data", function(data){
    file.write(data);
  });
  key_dest.on("end", function(data){
    file.end();
    run(e, ctx);
  });
}

If Anybody actually tries this out, please share the results. I would appreciate it. Then I should be able to merge this to master much faster.

Won't close this until I merge this with master.

Also, please feel free to point out if I'm missing anything from the diff

seletz commented 7 years ago

Oh WOW.

elfeffe commented 7 years ago

I will try it Thank you very much for this and for the whole project, this is a fantastic project.

seletz commented 7 years ago

Ok, so I allocated some time to get this working with Azure Notification Hub -- just because we happen to have some Azure stuff running internally. I probably need to hack the code a bit for that to work, will do on a separate branch.

seletz commented 7 years ago

Just wanted to mention that this code works beautifully. I think this issue could be closed now.

gliechtenstein commented 7 years ago

I also confirm that this works fine, but there's one thing that's been bothering me, which is why I didn't merge this to master yet: https://github.com/Jasonette/JASONETTE-iOS/issues/53

I hope some has a good solution for this!

tomershvueli commented 7 years ago

I was able to use this branch and push notifications worked well! I did have to enable the 'capability' manually. Also, I am using Firebase for pushes, so I had to adjust the JasonAppDelegate to register with the proper endpoints.

mrshawnj commented 6 years ago

this version seems to be missing file:// .... how do we reference local files from settings.plist?