justdmitry / PassKitHelper

Helper library for all your Apple PassKit (Apple Wallet, Apple Passbook) needs.
MIT License
26 stars 6 forks source link

How can we use the send push notification #16

Closed DboukAli98 closed 2 years ago

DboukAli98 commented 2 years ago

Hi Dmitry , thanks for this amazing library it helped me a lot so far , but I if you can please clarify more how the RegisterDeviceAsync() sendPushNotificaion() feature can be implemented to be able to send notifications on pass updates . Thanks in advance

justdmitry commented 2 years ago

When pass is added to new device - you will receive RegisterDeviceAsync callback with pass serialNumber (so you know what pass is this, who is owner) and pushToken. Save this pushToken in your database. When you need to update pass on user device (when something changed in user profile) - get pushToken from database and call SendPushNotificationAsync(). This will trigger update of this pass on users device (like when user pull down pass manually).

DboukAli98 commented 2 years ago

Thanks for your reply could you please provide an example on the RegisterDeviceAsync how I will actually use it

justdmitry commented 2 years ago

It depends how you create the passes. You need to put "serialNumber" there. Usually you should put different values for every user. So the same value will return to you in RegisterDeviceAsync() and you will know what pass=user is this.

DboukAli98 commented 2 years ago

Okay thanks man

DboukAli98 commented 2 years ago

Hello Dimitry , Kindly I want to ask how do I see the response from the apple server that contains the pushToken and other elements ?

justdmitry commented 2 years ago

Not sure what you talking about... What kind of request (to Apple server) you want inspect response to? Usually pushToken and other are in request from Apple servers...

Anyway, you need set breakpoints or use network sniffing tool (Fiddler for example) or build your own version of library with additional logging and switch to it temporary.

justdmitry commented 2 years ago

Also, make sure you seen this doc: https://developer.apple.com/library/archive/documentation/PassKit/Reference/PassKit_WebService/WebService.html

safermgmt-bfoster commented 2 years ago

Hi @justdmitry, awesome library BTW! I have a similar question. I'm trying to understand how Apple knows what URL to talk to when the device is being registered. I have followed your code example and put this code on my server, but the RegisterDeviceAsync() is never being hit (I have logging statements in my code to verify).

Also, how do I test this locally? How could Apple identify a localhost callback URL?

Thanks in Advance!

justdmitry commented 2 years ago

Hi @safermgmt-bfoster

You need to complete this steps: 1) You need some publicly-available hostname for your server, so Apple server can call you. Localhost obviously won't work. Also Apple requires is to be HTTPS. 2) Embed your server URL into your passes with .WebService.AuthenticationToken("some-secret-string").WebServiceURL("https://yourserver.com/passcallbackpath"). You need Authentication Token to verify that call is arrived from your pass and not from malicious hacker, I make them unique for each user. Some docs are here. 3) Use app.UsePassKitMiddleware("/passcallbackpath"); in your Startup to attach PassKitHelper middleware. Obviously path here must match one in passes from (2). Of course it may be any valid value you want, just make sure to put same value inside pass. 4) Implement IPassService (say, MyPassService) and register it in Startup with services.AddSingleton<IPassService, MyPassService>();

Also check Apple's PassKit Web Service Reference.

DboukAli98 commented 2 years ago

It worked for me thanks ! Sorry for bothering you I need to ask can I customize the send push notifications (I mean can I send promotional notifications through the wallet ) ?, and what are the limitations .

justdmitry commented 2 years ago

You can add changeMessage to field(s) in your pass and phone will display this message when this value changes. This message must include current field value with %@ placeholder, more details are here

I didn't seen any ways to send promotional/marketing pushes with passes.

DboukAli98 commented 2 years ago

hello , I want to kindly ask the SendPushNotificationsAsync() work only when I re-add the pass in the wallet (meaning when I download it with new values) I get push notifications but when the values changes and I don't re-add it manually I don't get push updates , can you please help about this ?

justdmitry commented 2 years ago

Did you implemented all methods of IPassKitService, including GetAssociatedPassesAsync too? Apple calls GetAssociatedPassesAsync to ask you "what passes were updated since X" so if you doesnt answer that pass number XXX updated - it may not be updated in phone.

Check "Getting the Serial Numbers for Passes Associated with a Device" here

DboukAli98 commented 2 years ago

Yes I implemented them all like that ` public class PassKitService : IPassKitService { private IConfiguration _config { get; } public Task<(int status, string[]? passes, string? tag)> GetAssociatedPassesAsync(string deviceLibraryIdentifier, string passTypeIdentifier, string? tag) { throw new NotImplementedException(); }

    public Task<(int statusCode, MemoryStream? passData, DateTimeOffset? lastModified)> GetPassAsync(string passTypeIdentifier, string serialNumber, string authorizationToken, DateTimeOffset? ifModifiedSince)
    {
        DateTimeOffset? modified = ifModifiedSince;
        string serial = serialNumber;
        string authToken = authorizationToken;
        string textToWrite = "pass with " + serial + " is modified since " + modified + " auth token is " + authToken;
        File.WriteAllTextAsync(@"E:\SME\Wallet.APITest\wwwroot\Pass.txt", textToWrite);
        throw new NotImplementedException();
    }

    public Task ProcessLogsAsync(string[] logs)
    {
        throw new NotImplementedException();
    }}`
DboukAli98 commented 2 years ago

Do you have any idea how I use GetAssociatedPassesAsync() or apple just automatically call it

justdmitry commented 2 years ago

throw new NotImplementedException();

So, you're not implemented them from Apple point of view.

Do you have any idea how I use GetAssociatedPassesAsync() or apple just automatically call it

Apple call it when phones asks "are there any updated passes". You must answer with list of "updated" passes and when they were updated last time. After that, phone(s) will call GetPassAsync for each obsolete pass. And if you don't answer GetAssociatedPassesAsync - it don't ask you for updated pass.

DboukAli98 commented 2 years ago

Hi @justdmitry, i would like to confirm how can i use the replacePassWithPass provided by Apple? i am unable to update the Pass after signing again.

Below are the steps that i am trying to match between your library and apple guidance:

To change a pass, coordinate with your server:

Your app connects to your server. It identifies the pass by serial number and pass type identifier and describes the change to your server.

Your server updates your business records as needed, creates a new version of the pass, and signs it.

Your app downloads the new pass from your server and uses the [replacePassWithPass:](https://developer.apple.com/documentation/passkit/pkpasslibrary/1617082-replacepasswithpass) method of the PKPassLibrary class to install it.

justdmitry commented 2 years ago

@DboukAli98 this method needed for apps that coded/runs directly in your iOS device. While PassKitHelper is for server-side code. Implement all five methods of IPassKitService and pass should start update.

DboukAli98 commented 2 years ago

@justdmitry thank you for your prompt support. Well i implemented all five methods of IPassKitService but my issue is that i am unable to see the pass updates when opening it. The only way to see the update is to regenerate a new pass with the same serial number and then clicking on add button in the Apple wallet IOS app will show me the updates. Is there another way to do it? My requirement is to update it without any need of redownloading the pass on the user device.

justdmitry commented 2 years ago

@DboukAli98

To see pass update you must deliver updated pass file to user. That's IPassKitService.GetPassAsync.

To get iPhone info what pass(es) are updated - you must answer to IPassKitService.GetAssociatedPassesAsync

To get iPhone "signal" that it need to check updates - you must call IPassKitHelper.SendPushNotificationAsync

To have pushToken - you must implement IPassKitHelper.RegisterDeviceAsync/UnregisterDeviceAsync

To know about "problems" with your implementation - you must implement IPassKitHelper.ProcessLogsAsync

That's the way for you if you have no native iOS app that can communicate with wallet "internally"

safermgmt-bfoster commented 1 year ago

@justdmitry, I have implemented all of the steps above, but for some reason either I'm missing a step or just not clear on how this all works (what part of this apple is responsible for vs what I'm responsible for). I have read through Apple's documentation, but that didn't provide too much success either.

I'm able to generate the pass successfully; download the pass to the iPhone. But when I call the SendNotificationAsync(pushToken) via PostMan, I get a response code of 200 and my GetAssociatedPassessAsync method gets called. What is the expected result after that? How should I handle this method?

I've hardcoded the GetAssociatedPassessAsync method with a real serial number (masked for security reasons) like this just to shortcut the full implementation.

public async Task<(int status, string[] passes, string tag)> GetAssociatedPassesAsync(string deviceLibraryIdentifier, string passTypeIdentifier, string tag)
        {
            string serials = "{\"lastUpdated\":\"" + DateTime.Now.ToString("yyyy-dd-mm hh:mm:ss") + "\", \"serialNumbers\": [\"e82xxxxx-xxxx-xxxx-xxxxx-xxxxxxxxx1ed\"]}";
            logger.Info($"GET ASSOCIATED PASSES: {deviceLibraryIdentifier} | {passTypeIdentifier} {(string.IsNullOrEmpty(tag) ? "No Tag" : tag)}");
            List<string> list = new List<string>();
            list.Add(serials);

            logger.Info($"GET serial numbers: {serials}");
            return (200, list.ToArray(), tag);
        }

I get the following logs written out on my server. I'm not sure what this error means or what I need to do at this point.

05:46:26 [INFO ] - GET serial numbers: {"lastUpdated":"2022-30-46 05:46:26", "serialNumbers": ["e82xxxxx-xxxx-xxxx-xxxxx-xxxxxxxxx1ed"]}
05:46:26 [INFO ] - PROCESS LOGS: [2022-11-30 17:46:26 -0600] Get serial #s task (for device {deviceId}, pass type [], last updated (null); with web service url https://{webserviceurl}.com/callback/passkit) encountered error: Server response was malformed (Wrong type object for key lastUpdated in response dictionary. Expected NSString but found NSNull.)

Any additional help you can provide, I'll be glad to receive it!

Regards

justdmitry commented 1 year ago

@safermgmt-bfoster You are very close! But... you put JSON string with dictionary into list. Wrong.

list must contain only pass serial numbers:

list.Add("e82xxxxx-xxxx-xxxx-xxxxx-xxxxxxxxx1ed");
var newTag = DateTimeOffset.UtcNow.ToString();
return (200, list.ToArray(), newTag);

Also, for your future code: Apple servers expect that you return (in list) only serials that had been changed since tag, so it will not re-request GetPassAsync for unchanged ones. You can return all serials of course, but Apple will throw you a warning in ProcessLogsAsync that you ignored tag and returned unchanged passes :)

safermgmt-bfoster commented 1 year ago

@justdmitry , you're awesome! That has got me another one step closer as I'm no longer getting the same error. New error: 06:00:43 [INFO ] - PROCESS LOGS: [2022-12-01 06:00:43 -0600] Get pass task (pass type [], serial number e82xxxxx-xxxx-xxxx-xxxxx-xxxxxxxxx1ed, if-modified-since (null); with web service url https://{webserviceurl}/callback/passkit) encountered error: Received invalid pass data (The pass cannot be read because it isn’t valid.)

Now when I send the serial numbers to Apple, I need to now send the Pass back to the user, but I'm not sure what format to send that data in.

I'm assuming the following method is being called. Do you have an example of what data (and format) that I'm passing back to the passData field?

public Task<(int statusCode, MemoryStream passData, DateTimeOffset? lastModified)> GetPassAsync(string passTypeIdentifier, string serialNumber, string authorizationToken, DateTimeOffset? ifModifiedSince)
 {

}
justdmitry commented 1 year ago

@safermgmt-bfoster Use MemoryStream that is returned from await passPackage.SignAndBuildAsync();

safermgmt-bfoster commented 1 year ago

@justdmitry, I hate to keep bothering you, but you are helping me so much. Finally got the MemoryStream passing back, but now I'm receiving the following error.

07:10:12 [INFO ] - PROCESS LOGS: [2022-12-01 07:10:12 -0600] Get pass task (pass type [], serial number e82xxxxx-xxxx-xxxx-xxxxx-xxxxxxxxx1ed, if-modified-since (null); with web service url https://{webserviceurl}/callback/passkit) encountered error: Server response was malformed (Missing response data)

Any thoughts around this one?

safermgmt-bfoster commented 1 year ago

@justdmitry, I think I figured out the previous error. Working on a new now. I think I'm finally seeing the light!!! :)

07:33:18 [ERROR] - Connection ID "17870283323017105792", Request ID "80059181-0000-f800-b63f-84710c7967bb": An unhandled exception was thrown by the application.
System.Exception: GetPassAsync() must return non-null 'lastModified' when 'status' == 200
   at PassKitHelper.PassKitMiddleware.InvokePassesAsync(HttpContext context, PathString passesRemainingPath)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT`1.ProcessRequestAsync()
safermgmt-bfoster commented 1 year ago

@justdmitry , hopefully this will be my last time pinging you. When I send back the pass as a MemoryStream, I'm getting this error now. Should the fieldName on the pass be unique each time I send it?

07:29:37 [INFO ] - PROCESS LOGS: [2022-12-01 07:29:37 -0600] Get pass task (pass type [], serial number e82xxxxx-xxxx-xxxx-xxxxx-xxxxxxxxx1ed, if-modified-since (null); with web service url https://{webserviceurl}/callback/passkit) warning: Received invalid pass data (more than one field has the key 'name'. Field keys must be unique.). This will be treated as an error in a future release. 
07:29:37 [INFO ] - PROCESS LOGS: [2022-12-01 07:29:37 -0600] Get pass task (pass type [], serial number e82xxxxx-xxxx-xxxx-xxxxx-xxxxxxxxx1ed if-modified-since (null); with web service url https://{webserviceurl}/callback/passkit) warning: Received invalid pass data (more than one field has the key 'id'. Field keys must be unique.). This will be treated as an error in a future release. 
07:29:37 [INFO ] - PROCESS LOGS: [2022-12-01 07:29:37 -0600] Get pass task (pass type [], serial number e82xxxxx-xxxx-xxxx-xxxxx-xxxxxxxxx1ed, if-modified-since (null); with web service url https://{webserviceurl}/callback/passkit) warning: Received invalid pass data (more than one field has the key 'title'. Field keys must be unique.). This will be treated as an error in a future release. 
justdmitry commented 1 year ago

@safermgmt-bfoster stop sending nulls everywhere and you should be fine :)

lastModified=null is only for non-200 response

Ideally, you should store something like "last change timestamp" internally for each you user/pass, so GetPassAsync will return same pass again and again until something changes, and that timestamp value as last-modified. And you should put that value into "tag" in GetAssociatedPassesAsync response; and parse it from tag param in GetAssociatedPassesAsync request and return empty 204 response if nothing changed.

justdmitry commented 1 year ago

more than one field has the key 'id'. Field keys must be unique

No, field IDs must be unique only inside single pass. New pass completely replaces old one (and I keep same field IDs for same fields). It seems that you either have two different fields with same ID, or accidentally added one field twice (and again, from iPhone point of view pass has two fields with same id). Check your pass generator code. Maybe you have some fields both in AddPassKitHelper/ConfigureNewPass and in passKitHelper.CreateNewPass().

safermgmt-bfoster commented 1 year ago

@justdmitry Thank you for ALL of your help! This was all so beneficial, I have a successful the test case now. I was able to see the digital ID card update with the Push Notification!!!!

justdmitry commented 1 year ago

@safermgmt-bfoster Congratulations!

It really seems like a magic when you have pass open on the phone, and after some update in database the pass updates itself in a couple of seconds 🧙‍♂️

Feel free to make a PR with more comments to classes/interfaces, or drop me a link if you will do it somewhere in your blog(s).