Search code examples
apple-push-notificationspassbookwalletpushsharp

Apple Wallet/Passbook notifications would not deliver


I am trying to send notification to update a digital card in wallet/passbook using Pushsharp. I have double-checked all these:

  • Use same certificate for signing the passes and updating them
  • Disabled the production/sandbox certificate check in Pushsharp
  • Using the pushtoken that I got from the device. Its in this format: d30720c34af46d65e02db3c3db6Ohae04d183dfaa105133f7c21b8d1963629fe
  • Generated .p12 certificate using this tutorial except following Step # 10: https://support.airship.com/hc/en-us/articles/213493683-How-to-make-an-Apple-Pass-Type-Certificate
  • telnet feedback.push.apple.com 2196 is successfull
  • The PassTypeIdentifier in pass.json is same as common name of the .p12 file
  • The card opens perfectly on the device
  • The device sends request for registering on the server
  • Pull-down update works perfectly

But when I send the request for notification to APNs, the device does not hits back.

Note: I have either changed or removed urls, tokens and paths from code below because of confidentiality

Below is the code that I am using to send notification

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using PushSharp.Apple;

namespace NotificationPushsharp01
{
    class Program
    {
        static void Main(string[] args)
        {
            string certificatePath = @"PATH_OF_CERTIFICATE.p12";
            X509Certificate2 clientCertificate = new X509Certificate2(System.IO.File.ReadAllBytes(certificatePath), "12345");

            // Configuration (NOTE: .pfx can also be used here)
            var config = new ApnsConfiguration(ApnsConfiguration.ApnsServerEnvironment.Production, clientCertificate, validateIsApnsCertificate: false);

            // Create a new broker
            var apnsBroker = new ApnsServiceBroker(config);

            System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;

            // Wire up events
            apnsBroker.OnNotificationFailed += (notification, aggregateEx) =>
            {

                aggregateEx.Handle(ex =>
                {

                    // See what kind of exception it was to further diagnose
                    if (ex is ApnsNotificationException notificationException)
                    {

                        // Deal with the failed notification
                        var apnsNotification = notificationException.Notification;
                        var statusCode = notificationException.ErrorStatusCode;

                        Console.WriteLine($"Apple Notification Failed: ID={apnsNotification.Identifier}, Code={statusCode}");

                    }
                    else
                    {
                        // Inner exception might hold more useful information like an ApnsConnectionException           
                        Console.WriteLine($"Apple Notification Failed for some unknown reason : {ex.InnerException}");
                    }

                    // Mark it as handled
                    return true;
                });
            };

            apnsBroker.OnNotificationSucceeded += (notification) =>
            {
                Console.WriteLine("Apple Notification Sent!");
            };

            // Start the broker
            apnsBroker.Start();

            apnsBroker.QueueNotification(new ApnsNotification
            {
                DeviceToken = "d30710c34af48d65e02db4c3db60fae04d283efaa105633f7c41b8d1963628fe",                 
                Payload = JObject.Parse("{\"aps\":\"\"}")
            });

            // Stop the broker, wait for it to finish   
            // This isn't done after every message, but after you're
            // done with the broker
            apnsBroker.Stop();

            Console.ReadLine();
        }
    }
}

Output of running the above code

2019-12-19 08:39:24.AM [DEBUG] Scaled Changed to: 1
2019-12-19 08:39:24.AM [INFO] Stopping: Waiting on Tasks
2019-12-19 08:39:24.AM [INFO] Waiting on all tasks 1
2019-12-19 08:39:25.AM [INFO] APNS-Client[1]: Sending Batch ID=1, Count=1
2019-12-19 08:39:25.AM [INFO] APNS-Client[1]: Connecting (Batch ID=1)
2019-12-19 08:39:25.AM [INFO] APNS-Client[1]: Connected (Batch ID=1)
2019-12-19 08:39:25.AM [INFO] APNS-Client[1]: Sent Batch, waiting for possible response...
2019-12-19 08:39:26.AM [INFO] APNS-Client[1]: Received -1 bytes response...
2019-12-19 08:39:26.AM [INFO] APNS-Client[1]: Batch (ID=1) completed with no error response...
2019-12-19 08:39:26.AM [INFO] APNS-Client[1]: Done Reading for Batch ID=1, reseting batch timer...
Apple Notification Sent!
2019-12-19 08:39:26.AM [INFO] All Tasks Finished
2019-12-19 08:39:26.AM [INFO] Passed WhenAll
2019-12-19 08:39:26.AM [INFO] Broker IsCompleted
2019-12-19 08:39:26.AM [DEBUG] Broker Task Ended
2019-12-19 08:39:26.AM [INFO] Stopping: Done Waiting on Tasks

The controller of API that's handling the registration and updates of pass

//Sorry for the comments
public class DevicesController : ApiController
{
    // GET request to webServiceURL/version/devices/deviceLibraryIdentifier/registrations/passTypeIdentifier
    [HttpGet]
    public HttpResponseMessage GetSerialNumber(string deviceLibraryIdentifier, string passTypeIdentifier)
    {
        System.IO.File.AppendAllText(@"updates.txt", "Device requested serial numbers S");
        System.IO.File.AppendAllText(@"updates.txt", System.Environment.NewLine);

        // For example...
        SerialNumbers lastUpdateToSerialNumDict = new SerialNumbers();
        // LastUpdated timestamp set to current datetime
        lastUpdateToSerialNumDict.lastUpdated = String.Format("{0:MM/dd/yyyy HH:mm:ss}", DateTime.Now);
        // A list of serial numbers got from database
        lastUpdateToSerialNumDict.serialNumbers = new List<string>();
        lastUpdateToSerialNumDict.serialNumbers.Add("123456789");
        string jsonRes = JsonConvert.SerializeObject(lastUpdateToSerialNumDict);
        HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
        response.Content = new StringContent(jsonRes, Encoding.UTF8, "application/json");
        return response;
    }

    // GET request to webServiceURL/version/devices/deviceLibraryIdentifier/registrations/passTypeIdentifier?passesUpdatedSince=tag
    [HttpGet]
    public HttpResponseMessage GetSerialNumber(string deviceLibraryIdentifier, string passTypeIdentifier, string passesUpdatedSince)
    {
        System.IO.File.AppendAllText(@"updates.txt", "Device requested serial numbers C");
        System.IO.File.AppendAllText(@"updates.txt", System.Environment.NewLine);

        // For example...
        SerialNumbers lastUpdateToSerialNumDict = new SerialNumbers();
        // LastUpdated timestamp set to current datetime
        lastUpdateToSerialNumDict.lastUpdated = String.Format("{0:MM/dd/yyyy HH:mm:ss}", DateTime.Now);
        // A list of serial numbers got from database
        lastUpdateToSerialNumDict.serialNumbers = new List<string>();
        lastUpdateToSerialNumDict.serialNumbers.Add("123456789");
        string jsonRes = JsonConvert.SerializeObject(lastUpdateToSerialNumDict);
        HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
        response.Content = new StringContent(jsonRes, Encoding.UTF8, "application/json");
        return response;
    }

    // POST request to webServiceURL/version/devices/deviceLibraryIdentifier/registrations/passTypeIdentifier/serialNumber
    [HttpPost]
    public HttpResponseMessage RegisterDevice(string deviceLibraryIdentifier, string passTypeIdentifier, string serialNumber, [FromBody]HR.CardApi.Api.Models.DevicesPayload payload)
    {


        //ApiLog
        System.IO.File.AppendAllText(@"ApiLog.txt", "devices/register was used");
        System.IO.File.AppendAllText(@"D:\FGC-Microservice\HR.CardApi.DDC_31072019\HR.CardApi.Api\bin\ApiLog.txt", System.Environment.NewLine);
        //ApiLog

        System.IO.File.AppendAllText(@"data.txt", deviceLibraryIdentifier);
        System.IO.File.AppendAllText(@"data.txt", System.Environment.NewLine);

        System.IO.File.AppendAllText(@"data.txt", passTypeIdentifier);
        System.IO.File.AppendAllText(@"data.txt", System.Environment.NewLine);

        System.IO.File.AppendAllText(@"data.txt", serialNumber);
        System.IO.File.AppendAllText(@"data.txt", System.Environment.NewLine);

        System.IO.File.AppendAllText(@"data.txt", payload.pushToken);
        System.IO.File.AppendAllText(@"data.txt", System.Environment.NewLine);
        System.IO.File.AppendAllText(@"data.txt", System.Environment.NewLine);

        System.Net.Http.HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
        return response;
    }

    // DELETE request to webServiceURL/version/devices/deviceLibraryIdentifier/registrations/passTypeIdentifier/serialNumber
    [HttpDelete]
    public HttpResponseMessage UnRegisterDevice(string deviceLibraryIdentifier, string passTypeIdentifier, string serialNumber)
    {
        //Udpate Devices and Register table
        System.Net.Http.HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
        return response;
    }
}

pass.json of pass.pkpass file

{
  "passTypeIdentifier": "SAME_AS_IN_KEYCHAIN",
  "formatVersion": 1,
  "serialNumber": "123456789",
  "description": "CONFIDENTIAL",
  "organizationName": "CONFIDENTIAL",
  "teamIdentifier": "CONFIDENTIAL",
  "voided": false,
  "barcodes": [
    {
      "format": "PKBarcodeFormatPDF417",
      "message": "1234587568464654",
      "messageEncoding": "ISO-8859-1"
    }
  ],
  "storeCard": {
    "headerFields": [],
    "primaryFields": [],
    "secondaryFields": [],
    "auxiliaryFields": [
      {
        "key": "offer",
        "label": "Initial Pass",
        "value": "8979787645464654"
      }
    ],
    "backFields": [
      {
        "key": "CONFIDENTIAL",
        "label": "CONFIDENTIAL",
        "attributedValue": "CONFIDENTIAL",
        "value": "CONFIDENTIAL"
      }
    ]
  },
  "authenticationToken": "wxyzd7J8AlYYFPS0k0a0FfVFtq0ewzEfd",
  "webServiceURL": "https://CONFIDENTIAL.com"
}

You can ask any question related to code or anything. Currently, I am inspecting the device log. I'll update if I find something useful.

Thank you for your time.


Solution

  • I figured out what the problem was. The code given in question is correct. There's no error in it.

    The API was returning HTTP 404 when the device was requesting serial numbers. This was because my pass's passTypeIdentifier contained dots in it. It was like pass.acbcd.abcd. I just configured my API to allow dots in the url and everything started working fine.