passbook2 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.travis.yml +7 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +136 -0
- data/LICENSE +22 -0
- data/README.md +271 -0
- data/Rakefile +8 -0
- data/VERSION +1 -0
- data/lib/passbook/pk_multi_pass.rb +26 -0
- data/lib/passbook/pkpass.rb +133 -0
- data/lib/passbook/push_notification.rb +19 -0
- data/lib/passbook/signer.rb +75 -0
- data/lib/passbook/version.rb +3 -0
- data/lib/passbook2.rb +21 -0
- data/passbook.gemspec +59 -0
- data/spec/data/icon.png +0 -0
- data/spec/data/icon@2x.png +0 -0
- data/spec/data/logo.png +0 -0
- data/spec/data/logo@2x.png +0 -0
- data/spec/lib/passbook/pk_multi_pass_spec.rb +67 -0
- data/spec/lib/passbook/pkpass_spec.rb +169 -0
- data/spec/lib/passbook/push_notification_spec.rb +23 -0
- data/spec/lib/passbook/signer_spec.rb +100 -0
- data/spec/spec_helper.rb +8 -0
- metadata +156 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4a58374078ae35a90f7e9704ff3e643e6936dc64f8d218861f0e4e5fbc93edfb
|
4
|
+
data.tar.gz: 4d1ff0d20b11ee00f5b2062d83dc48979e7b5c1e9916ff401843709a7f309217
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dcc263f3738566ddc9eddc287330bde5908e4503755c71bbdd872679258e3c38b97681fd8e021fff443d243de143cddbc88eab56ba07014d4f58e77a6c23c9b5
|
7
|
+
data.tar.gz: f1f04770847c5e8c26a5ab2354d5dae23f3555e372dafc73eebb5baa317fd2ac87695bb042c4ad29ff04d1981e6630704cf4f91b7e985488dcc1fb976ebc93e6
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in passbook.gemspec
|
4
|
+
gem 'rubyzip', '>= 1.0.0'
|
5
|
+
gem 'grocer'
|
6
|
+
gem 'commander'
|
7
|
+
gem 'terminal-table'
|
8
|
+
|
9
|
+
group :test, :development do
|
10
|
+
gem 'rack-test'
|
11
|
+
gem 'activesupport'
|
12
|
+
gem 'jeweler'
|
13
|
+
gem 'simplecov'
|
14
|
+
gem 'rspec'
|
15
|
+
gem 'rake'
|
16
|
+
gem 'yard'
|
17
|
+
gem 'debug'
|
18
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activesupport (7.0.4.3)
|
5
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
6
|
+
i18n (>= 1.6, < 2)
|
7
|
+
minitest (>= 5.1)
|
8
|
+
tzinfo (~> 2.0)
|
9
|
+
addressable (2.4.0)
|
10
|
+
builder (3.2.4)
|
11
|
+
commander (4.6.0)
|
12
|
+
highline (~> 2.0.0)
|
13
|
+
concurrent-ruby (1.2.2)
|
14
|
+
debug (1.8.0)
|
15
|
+
irb (>= 1.5.0)
|
16
|
+
reline (>= 0.3.1)
|
17
|
+
descendants_tracker (0.0.4)
|
18
|
+
thread_safe (~> 0.3, >= 0.3.1)
|
19
|
+
diff-lcs (1.5.0)
|
20
|
+
docile (1.4.0)
|
21
|
+
faraday (0.9.2)
|
22
|
+
multipart-post (>= 1.2, < 3)
|
23
|
+
git (1.11.0)
|
24
|
+
rchardet (~> 1.8)
|
25
|
+
github_api (0.16.0)
|
26
|
+
addressable (~> 2.4.0)
|
27
|
+
descendants_tracker (~> 0.0.4)
|
28
|
+
faraday (~> 0.8, < 0.10)
|
29
|
+
hashie (>= 3.4)
|
30
|
+
mime-types (>= 1.16, < 3.0)
|
31
|
+
oauth2 (~> 1.0)
|
32
|
+
grocer (0.7.1)
|
33
|
+
hashie (5.0.0)
|
34
|
+
highline (2.0.3)
|
35
|
+
i18n (1.13.0)
|
36
|
+
concurrent-ruby (~> 1.0)
|
37
|
+
io-console (0.6.0)
|
38
|
+
io-console (0.6.0-java)
|
39
|
+
irb (1.7.4)
|
40
|
+
reline (>= 0.3.6)
|
41
|
+
jar-dependencies (0.4.1)
|
42
|
+
jeweler (2.3.9)
|
43
|
+
builder
|
44
|
+
bundler
|
45
|
+
git (>= 1.2.5)
|
46
|
+
github_api (~> 0.16.0)
|
47
|
+
highline (>= 1.6.15)
|
48
|
+
nokogiri (>= 1.5.10)
|
49
|
+
psych
|
50
|
+
rake
|
51
|
+
rdoc
|
52
|
+
semver2
|
53
|
+
jwt (2.7.0)
|
54
|
+
mime-types (2.99.3)
|
55
|
+
mini_portile2 (2.8.1)
|
56
|
+
minitest (5.18.0)
|
57
|
+
multi_json (1.15.0)
|
58
|
+
multi_xml (0.6.0)
|
59
|
+
multipart-post (2.3.0)
|
60
|
+
nokogiri (1.14.3)
|
61
|
+
mini_portile2 (~> 2.8.0)
|
62
|
+
racc (~> 1.4)
|
63
|
+
nokogiri (1.14.3-java)
|
64
|
+
racc (~> 1.4)
|
65
|
+
oauth2 (1.4.8)
|
66
|
+
faraday (>= 0.8, < 3.0)
|
67
|
+
jwt (>= 1.0, < 3.0)
|
68
|
+
multi_json (~> 1.3)
|
69
|
+
multi_xml (~> 0.5)
|
70
|
+
rack (>= 1.2, < 3)
|
71
|
+
psych (5.1.0)
|
72
|
+
stringio
|
73
|
+
psych (5.1.0-java)
|
74
|
+
jar-dependencies (>= 0.1.7)
|
75
|
+
racc (1.6.2)
|
76
|
+
racc (1.6.2-java)
|
77
|
+
rack (2.2.7)
|
78
|
+
rack-test (2.1.0)
|
79
|
+
rack (>= 1.3)
|
80
|
+
rake (13.0.6)
|
81
|
+
rchardet (1.8.0)
|
82
|
+
rdoc (6.5.0)
|
83
|
+
psych (>= 4.0.0)
|
84
|
+
reline (0.3.8)
|
85
|
+
io-console (~> 0.5)
|
86
|
+
rspec (3.12.0)
|
87
|
+
rspec-core (~> 3.12.0)
|
88
|
+
rspec-expectations (~> 3.12.0)
|
89
|
+
rspec-mocks (~> 3.12.0)
|
90
|
+
rspec-core (3.12.2)
|
91
|
+
rspec-support (~> 3.12.0)
|
92
|
+
rspec-expectations (3.12.3)
|
93
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
94
|
+
rspec-support (~> 3.12.0)
|
95
|
+
rspec-mocks (3.12.5)
|
96
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
97
|
+
rspec-support (~> 3.12.0)
|
98
|
+
rspec-support (3.12.0)
|
99
|
+
rubyzip (2.3.2)
|
100
|
+
semver2 (3.4.2)
|
101
|
+
simplecov (0.22.0)
|
102
|
+
docile (~> 1.1)
|
103
|
+
simplecov-html (~> 0.11)
|
104
|
+
simplecov_json_formatter (~> 0.1)
|
105
|
+
simplecov-html (0.12.3)
|
106
|
+
simplecov_json_formatter (0.1.4)
|
107
|
+
stringio (3.0.6)
|
108
|
+
terminal-table (3.0.2)
|
109
|
+
unicode-display_width (>= 1.1.1, < 3)
|
110
|
+
thread_safe (0.3.6)
|
111
|
+
thread_safe (0.3.6-java)
|
112
|
+
tzinfo (2.0.6)
|
113
|
+
concurrent-ruby (~> 1.0)
|
114
|
+
unicode-display_width (2.4.2)
|
115
|
+
yard (0.9.34)
|
116
|
+
|
117
|
+
PLATFORMS
|
118
|
+
java
|
119
|
+
ruby
|
120
|
+
|
121
|
+
DEPENDENCIES
|
122
|
+
activesupport
|
123
|
+
commander
|
124
|
+
debug
|
125
|
+
grocer
|
126
|
+
jeweler
|
127
|
+
rack-test
|
128
|
+
rake
|
129
|
+
rspec
|
130
|
+
rubyzip (>= 1.0.0)
|
131
|
+
simplecov
|
132
|
+
terminal-table
|
133
|
+
yard
|
134
|
+
|
135
|
+
BUNDLED WITH
|
136
|
+
2.4.10
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Thomas Lauro, Lance Gleason, 2024 Kay Rhodes
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,271 @@
|
|
1
|
+
# passbook2
|
2
|
+
|
3
|
+
The passbook2 gem let's you create a pkpass files for Apple's PassKit.
|
4
|
+
It is a fork of the "passbook" gem that has been updated to work with OpenSSL 3.0 and Ruby 3.x. It no longer uses p12 files which because they are no longer supported by Apple.
|
5
|
+
|
6
|
+
Apple's [PassKit](https://developer.apple.com/documentation/passkit_apple_pay_and_wallet) is used for processing Apple Pay payments & distributing tickets. This library is currently concerned with simplifying the process of generating a `.pass` bundle for distribution to your users. It's been thoroughly tested as a means of distributing even tickets to iOS devices.
|
7
|
+
|
8
|
+
This library does _not_ address updating information on tickets you've already distributed.
|
9
|
+
|
10
|
+
Note: This is a fork of [the original passbook gem](https://github.com/frozon/passbook). In addition to a lot of cleanup, it has been updated to support Ruby 3.0 and Apple's current cryptographic requirements.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Include the passbook2 gem in your project.
|
15
|
+
|
16
|
+
```
|
17
|
+
gem 'passbook2'
|
18
|
+
```
|
19
|
+
|
20
|
+
## Configuration
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
|
26
|
+
# Note: the pb_confg variable below is typically a singleton
|
27
|
+
# configuration object that has knowledge of where you store
|
28
|
+
# your certificate files & related passwords
|
29
|
+
# You definitely should not be hardcoding that information.
|
30
|
+
pb_config = <your config object>
|
31
|
+
|
32
|
+
Passbook.configure do |passbook|
|
33
|
+
|
34
|
+
# Path to your wwdc cert file# path to the latest
|
35
|
+
# "Apple Intermediate Certificate Worldwide Developer Relations" certificate
|
36
|
+
# (.cer) file downloaded from here https://www.apple.com/certificateauthority/
|
37
|
+
# and placed somewhere under the rails root. It doesn't matter where.
|
38
|
+
# ⚠❗ these expire
|
39
|
+
passbook.apple_intermediate_cert = pb_config.apple_intermediate_certificate
|
40
|
+
|
41
|
+
# Path to your X509 cert. This is downloaded after generating
|
42
|
+
# a certificate from your apple Pass Type ID on apple's developer site
|
43
|
+
passbook.certificate = pb_config.x509_certificate
|
44
|
+
|
45
|
+
# Path to the .pem file generated from public key of the RSA keypair
|
46
|
+
# that was generated when you made a Certificate Signing Request
|
47
|
+
# It'll be in your keychain under the "Common Name" you specified
|
48
|
+
# for the signing request.
|
49
|
+
passbook.rsa_private_key = pb_config.private_key_pem
|
50
|
+
|
51
|
+
# Password for the .pem file above
|
52
|
+
passbook.password = pb_config.rsa_password
|
53
|
+
end
|
54
|
+
|
55
|
+
```
|
56
|
+
|
57
|
+
Additional (optional) configuration variables:
|
58
|
+
- `notification_cert`
|
59
|
+
- `notification_gateway`
|
60
|
+
- `notification_passphrase`
|
61
|
+
|
62
|
+
|
63
|
+
## Usage
|
64
|
+
|
65
|
+
First create a new `PKPass` object and pass it your pass' JSON data in the initializer. This will be stored in the pass' `pass.json` file.
|
66
|
+
|
67
|
+
``` ruby
|
68
|
+
pass = Passbook::PKPass.new('{"YOUR STRINGIFIED JSON DATA"}')
|
69
|
+
```
|
70
|
+
|
71
|
+
Then iterate over the files you need to add to the pass. The order you add them in doesn't matter, but the names must adhere to Apple's Passbook naming convention.
|
72
|
+
|
73
|
+
``` ruby
|
74
|
+
pass.add_file("path/to/icon.png")
|
75
|
+
pass.add_files(["path/to/icon@2x.png", "path/to/thumbnail.png"])
|
76
|
+
```
|
77
|
+
|
78
|
+
![sample.pass](https://github.com/masukomi/passbook2/blob/master/docs/images/passbook_file_structure.png?raw=true)
|
79
|
+
|
80
|
+
Once you've added everything you need to your pass, it's time to generate the `.pkpass` zip file.
|
81
|
+
|
82
|
+
``` ruby
|
83
|
+
pass.file # returns a Tempfile containing a ZipStream
|
84
|
+
|
85
|
+
# you can optionally specify the file name and directory to store it in.
|
86
|
+
pass.file({file_name: 'pass.pkpass', directory: Dir.tmpdir})
|
87
|
+
|
88
|
+
```
|
89
|
+
|
90
|
+
In a Rails app you'd want to send the `.pkpass` data to users with the appropriate mime-type. Here's how to do that.
|
91
|
+
|
92
|
+
``` ruby
|
93
|
+
zip_file_path = my_pass.file.path
|
94
|
+
File.open(zip_file_path, "r") do |file|
|
95
|
+
send_data(file.read,
|
96
|
+
type: "application/vnd.apple.pkpass",
|
97
|
+
disposition: "attachment",
|
98
|
+
filename: "#{thingy.descriptive_name}.pkpass")
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
### Creating MultiPasses
|
103
|
+
|
104
|
+
Apple supports the idea of a MultiPass. This is just a zipped collection of multiple passes. If, for example, someone buys 5 tickets, you can send them a MultiPass with all 5 wallet passes in it.
|
105
|
+
|
106
|
+
A MultiPass must contain between 1 and 10 passes (inclusive). If your user needs more than 10 items you'll just have to convince them to download multiple MultiPasses. This is annoying, but it's Apple's restriction so there's nothing we can do about it.
|
107
|
+
|
108
|
+
To create a MultiPass simply call `PKMultiPass.create_multipass` and give it an array of `PKPass` objects and a valid path and filename to store your MultiPass in. Note that it must end with `.pkpasses`
|
109
|
+
|
110
|
+
``` ruby
|
111
|
+
my_file_path = File.join(temp_dir, "#{my_order_number}.pkpasses")
|
112
|
+
multipass_zip_file_path = Passbook::PKMultiPass.create_multipass(
|
113
|
+
array_of_pk_pass_objects, my_file_path
|
114
|
+
)
|
115
|
+
|
116
|
+
```
|
117
|
+
This will return you the same path you passed in. The resulting `.pkpasses` zip file will use ordinal numbers for the names of the `.pkpass` files inside. E.g. `1.pkpass`, `2.pkpass`, `3.pkpass`, etc.
|
118
|
+
|
119
|
+
Note: if you know where the current documentation for creating MultiPass files is, please let me know.
|
120
|
+
|
121
|
+
Sending this data to users is exactly the same as sending the data for an individual pass.
|
122
|
+
|
123
|
+
-----
|
124
|
+
|
125
|
+
### Original Passbook(1) functionality
|
126
|
+
⚠ WARNING
|
127
|
+
|
128
|
+
The following features & documentation come from the original passbook gem, and have _not_ been tested with Apple's current requirements. Signing _does_ work by default, but custom signing has not been tested and appears to still be thinking in terms of `.p12` files which Apple no longer honors.
|
129
|
+
|
130
|
+
Please make a PR if you make an updated version of any of this.
|
131
|
+
|
132
|
+
|
133
|
+
#### Using Different Certificates For Different Passes
|
134
|
+
|
135
|
+
Sometime you might want to be able to use different certificates for different passes. This can be done by passing in a Signer class into your PKPass initializer. You don't have to use environment variables, but it's a good way to make these things easy to rotate in the future when the certs expire.
|
136
|
+
|
137
|
+
```
|
138
|
+
signer = Passbook::Signer.new(
|
139
|
+
certificate: Rails.root.join(ENV['PASSBOOK_X509_CERTIFICATE']),
|
140
|
+
rsa_private_key: Rails.root.join(ENV['PASSBOOK_PRIVATE_KEY_PEM']),
|
141
|
+
password: ENV['PASSBOOK_RSA_PASSWORD'],
|
142
|
+
apple_intermediate_cert: Rails.root.join(ENV['PASSBOOK_APPLE_INTERMEDIATE_CERTIFICATE'])
|
143
|
+
)
|
144
|
+
pk_pass = Passbook::PKPass.new(data, signer)
|
145
|
+
|
146
|
+
....
|
147
|
+
```
|
148
|
+
|
149
|
+
### Push Notifications
|
150
|
+
|
151
|
+
If you want to support passbook push notification updates you will need to configure the appropriate bits above.
|
152
|
+
|
153
|
+
In order to support push notifications you will need to have a basic understanding of the way that push notifications work and how the data is passed back and forth.
|
154
|
+
|
155
|
+
Your pass will need to have a field called 'webServiceURL' with the base url to your site and a field called 'authenticationToken'. The json snippet should look like this. Note that your url needs to be a valid signed https endpoint for production. You can put your phone in dev mode to test updates against a insecure http endpoint (under settings => developer => passkit testing).
|
156
|
+
|
157
|
+
```
|
158
|
+
...
|
159
|
+
"webserviceURL" : "https://www.honeybadgers.com/",
|
160
|
+
"authenticationToken" : "yummycobras"
|
161
|
+
...
|
162
|
+
```
|
163
|
+
|
164
|
+
Passbook includes rack middleware to make the job of supporting the passbook endpoints easier. You will need to configure the middleware as outlined above and then implement a class called Passbook::PassbookNotification. Below is an annotated implementation.
|
165
|
+
|
166
|
+
```
|
167
|
+
module Passbook
|
168
|
+
class PassbookNotification
|
169
|
+
|
170
|
+
# This is called whenever a new pass is saved to a users passbook or the
|
171
|
+
# notifications are re-enabled. You will want to persist these values to
|
172
|
+
# allow for updates on subsequent calls in the call chain. You can have
|
173
|
+
# multiple push tokens and serial numbers for a specific
|
174
|
+
# deviceLibraryIdentifier.
|
175
|
+
|
176
|
+
def self.register_pass(options)
|
177
|
+
the_passes_serial_number = options['serialNumber']
|
178
|
+
the_devices_device_library_identifier = options['deviceLibraryIdentifier']
|
179
|
+
the_devices_push_token = options['pushToken']
|
180
|
+
the_pass_type_identifier = options["passTypeIdentifier"]
|
181
|
+
the_authentication_token = options['authToken']
|
182
|
+
|
183
|
+
# this is if the pass registered successfully
|
184
|
+
# change the code to 200 if the pass has already been registered
|
185
|
+
# 404 if pass not found for serialNubmer and passTypeIdentifier
|
186
|
+
# 401 if authorization failed
|
187
|
+
# or another appropriate code if something went wrong.
|
188
|
+
{:status => 201}
|
189
|
+
end
|
190
|
+
|
191
|
+
# This is called when the device receives a push notification from apple.
|
192
|
+
# You will need to return the serial number of all passes associated with
|
193
|
+
# that deviceLibraryIdentifier.
|
194
|
+
|
195
|
+
def self.passes_for_device(options)
|
196
|
+
device_library_identifier = options['deviceLibraryIdentifier']
|
197
|
+
passes_updated_since = options['passesUpdatedSince']
|
198
|
+
|
199
|
+
# the 'lastUpdated' uses integers values to tell passbook if the pass is
|
200
|
+
# more recent than the current one. If you just set it is the same value
|
201
|
+
# every time the pass will update and you will get a warning in the log files.
|
202
|
+
# you can use the time in milliseconds, a counter or any other numbering scheme.
|
203
|
+
# you then also need to return an array of serial numbers.
|
204
|
+
{'lastUpdated' => '1', 'serialNumbers' => ['various', 'serial', 'numbers']}
|
205
|
+
end
|
206
|
+
|
207
|
+
# this is called when a pass is deleted or the user selects the option to disable pass updates.
|
208
|
+
def self.unregister_pass(options)
|
209
|
+
# a solid unique pair of identifiers to identify the pass are
|
210
|
+
serial_number = options['serialNumber']
|
211
|
+
device_library_identifier = options['deviceLibraryIdentifier']
|
212
|
+
the_pass_type_identifier = options["passTypeIdentifier"]
|
213
|
+
the_authentication_token = options['authToken']
|
214
|
+
# return a status 200 to indicate that the pass was successfully unregistered.
|
215
|
+
{:status => 200}
|
216
|
+
end
|
217
|
+
|
218
|
+
# this returns your updated pass
|
219
|
+
def self.latest_pass(options)
|
220
|
+
the_pass_serial_number = options['serialNumber']
|
221
|
+
# create your PkPass the way you did when your first created the pass.
|
222
|
+
# you will want to return
|
223
|
+
my_pass = PkPass.new 'your pass json'
|
224
|
+
# you will want to return the string from the stream of your PkPass object.
|
225
|
+
{:status => 200, :latest_pass => mypass.stream.string, :last_modified => '1442120893'}
|
226
|
+
end
|
227
|
+
|
228
|
+
# This is called whenever there is something from the update process that is a warning
|
229
|
+
# or error
|
230
|
+
def self.passbook_log(log)
|
231
|
+
# this is a VERY crude logging example. use the logger of your choice here.
|
232
|
+
p "#{Time.now} #{log}"
|
233
|
+
end
|
234
|
+
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
```
|
239
|
+
|
240
|
+
To send a push notification for a updated pass simply call Passbook::PushNotification.send_notification with the push token for the device you are updating
|
241
|
+
|
242
|
+
```
|
243
|
+
Passbook::PushNotification.send_notification the_device_push_token
|
244
|
+
|
245
|
+
```
|
246
|
+
|
247
|
+
Apple will send out a notification to your phone (usually within 15 minutes or less), which will cause the phone that this push notification is associated with to make a call to your server to get pass serial numbers and to then get the updated pass. Each phone/pass combination has it's own push token whch will require a separate call for every phone that has push notifications enabled for a pass (this is an Apple thing). In the future we may look into offering background process support for this as part of this gem. For now, if you have a lot of passes to update you will need to do this yourself.
|
248
|
+
|
249
|
+
## Tests
|
250
|
+
|
251
|
+
To launch tests:
|
252
|
+
|
253
|
+
```bash
|
254
|
+
bundle exec rake spec
|
255
|
+
```
|
256
|
+
|
257
|
+
## Contributing
|
258
|
+
|
259
|
+
1. Fork it
|
260
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
261
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
262
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
263
|
+
5. Create new Pull Request
|
264
|
+
|
265
|
+
|
266
|
+
License
|
267
|
+
-------
|
268
|
+
|
269
|
+
passbook is released under the MIT license:
|
270
|
+
|
271
|
+
* http://www.opensource.org/licenses/MIT
|
data/Rakefile
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.5.1
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'zip'
|
2
|
+
require 'fileutils'
|
3
|
+
module Passbook
|
4
|
+
class PKMultiPass
|
5
|
+
def self.create_multipass(pk_passes, multipass_file_path)
|
6
|
+
raise "Too many passes" if pk_passes.size > 10
|
7
|
+
raise "No passes provided" if pk_passes.size < 1
|
8
|
+
raise "multipass files must have the .pkpasses extension" \
|
9
|
+
unless multipass_file_path.end_with? ".pkpasses"
|
10
|
+
raise "passes must be PKPass objects" \
|
11
|
+
unless pk_passes.all? { |pass| pass.is_a? Passbook::PKPass }
|
12
|
+
temp_dir = Dir.mktmpdir
|
13
|
+
pass_counter = 0
|
14
|
+
Zip::File.open(multipass_file_path, create: true) do |zipfile|
|
15
|
+
pk_passes.each do | pass |
|
16
|
+
pass_counter += 1
|
17
|
+
file_name = "#{pass_counter}.pkpass"
|
18
|
+
pkpass_path = pass.file(file_name: file_name, directory: temp_dir)
|
19
|
+
zipfile.add(file_name, pkpass_path)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
FileUtils.remove_dir(temp_dir, true)
|
23
|
+
multipass_file_path
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require 'openssl'
|
3
|
+
require 'zip'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module Passbook
|
7
|
+
class PKPass
|
8
|
+
attr_accessor :pass, :manifest_files, :signer
|
9
|
+
|
10
|
+
TYPES = %w(boarding-pass coupon event-ticket store-card generic)
|
11
|
+
|
12
|
+
# Initializes a new Passbook::PKPass object
|
13
|
+
# @param pass [String] JSON string representing the pass
|
14
|
+
# @param init_signer [Passbook::Signer] (optional) a signer object
|
15
|
+
def initialize(pass, init_signer = nil)
|
16
|
+
@pass = pass
|
17
|
+
@manifest_files = []
|
18
|
+
@signer = init_signer || Passbook::Signer.new
|
19
|
+
end
|
20
|
+
|
21
|
+
# Adds a single file to the pass
|
22
|
+
def add_file(file)
|
23
|
+
@manifest_files << file
|
24
|
+
end
|
25
|
+
|
26
|
+
# Adds multiple files to the pass
|
27
|
+
def add_files(files_array)
|
28
|
+
@manifest_files += files_array
|
29
|
+
end
|
30
|
+
|
31
|
+
# List of files in the pass
|
32
|
+
def list_files
|
33
|
+
@manifest_files
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
# Return a Tempfile containing our ZipStream
|
39
|
+
# @param options [Hash] :file_name, :directory
|
40
|
+
# - file_name defaults to 'pass.pkpass'
|
41
|
+
# a tempfile will be created and renamed to this
|
42
|
+
# - directory defaults to Dir.tmpdir
|
43
|
+
# and can be specifed as an absolute string path or a
|
44
|
+
# Dir object
|
45
|
+
def file(options = {})
|
46
|
+
options[:file_name] ||= 'pass.pkpass'
|
47
|
+
options[:directory] ||= Dir.tmpdir
|
48
|
+
desired_path = File.join(options[:directory], options[:file_name])
|
49
|
+
|
50
|
+
File.binwrite(desired_path, self.stream.string)
|
51
|
+
File.new(desired_path)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return a ZipOutputStream
|
55
|
+
def stream
|
56
|
+
manifest, signature = build
|
57
|
+
|
58
|
+
output_zip manifest, signature
|
59
|
+
end
|
60
|
+
|
61
|
+
# Builds the pass.
|
62
|
+
# Creates the manifest, checks for necessary files and fields
|
63
|
+
# Returns an array with the manifest and the pass' cryptographic signature
|
64
|
+
# Don't call this unless you have a specific need. Use #file or #stream instead
|
65
|
+
def build
|
66
|
+
manifest = create_manifest
|
67
|
+
|
68
|
+
# Check pass for necessary files and fields
|
69
|
+
check_pass manifest
|
70
|
+
|
71
|
+
# Create pass signature
|
72
|
+
signature = @signer.sign manifest
|
73
|
+
|
74
|
+
[manifest, signature]
|
75
|
+
end
|
76
|
+
|
77
|
+
## PRIVATE METHODS ##############################################################
|
78
|
+
private
|
79
|
+
|
80
|
+
def check_pass(manifest)
|
81
|
+
# Check for default images
|
82
|
+
raise 'Icon missing' unless manifest.include?('icon.png')
|
83
|
+
raise 'Icon@2x missing' unless manifest.include?('icon@2x.png')
|
84
|
+
|
85
|
+
# Check for developer field in JSON
|
86
|
+
raise 'Pass Type Identifier missing' unless @pass.include?('passTypeIdentifier')
|
87
|
+
raise 'Team Identifier missing' unless @pass.include?('teamIdentifier')
|
88
|
+
raise 'Serial Number missing' unless @pass.include?('serialNumber')
|
89
|
+
raise 'Organization Name Identifier missing' unless @pass.include?('organizationName')
|
90
|
+
raise 'Format Version' unless @pass.include?('formatVersion')
|
91
|
+
raise 'Format Version should be a numeric' unless JSON.parse(@pass)['formatVersion'].is_a?(Numeric)
|
92
|
+
raise 'Description' unless @pass.include?('description')
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_manifest
|
96
|
+
sha1s = {}
|
97
|
+
sha1s['pass.json'] = Digest::SHA1.hexdigest @pass
|
98
|
+
|
99
|
+
@manifest_files.each do |file|
|
100
|
+
if file.class == Hash
|
101
|
+
sha1s[file[:name]] = Digest::SHA1.hexdigest file[:content]
|
102
|
+
else
|
103
|
+
# either a File or a Pathname
|
104
|
+
sha1s[File.basename(file)] = Digest::SHA1.file(File.absolute_path(file)).hexdigest
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
sha1s.to_json
|
109
|
+
end
|
110
|
+
|
111
|
+
def output_zip(manifest, signature)
|
112
|
+
|
113
|
+
Zip::OutputStream.write_buffer do |zip|
|
114
|
+
zip.put_next_entry 'pass.json'
|
115
|
+
zip.write @pass
|
116
|
+
zip.put_next_entry 'manifest.json'
|
117
|
+
zip.write manifest
|
118
|
+
zip.put_next_entry 'signature'
|
119
|
+
zip.write signature
|
120
|
+
|
121
|
+
@manifest_files.each do |file|
|
122
|
+
if file.class == Hash
|
123
|
+
zip.put_next_entry file[:name]
|
124
|
+
zip.print file[:content]
|
125
|
+
else
|
126
|
+
zip.put_next_entry File.basename(file)
|
127
|
+
zip.print IO.read(file)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Passbook
|
2
|
+
class PushNotification
|
3
|
+
class << self
|
4
|
+
def pusher
|
5
|
+
@pusher ||= Grocer.pusher(
|
6
|
+
:certificate => Passbook.notification_cert,
|
7
|
+
:passphrase => Passbook.notification_passphrase || "",
|
8
|
+
:gateway => Passbook.notification_gateway
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
def send_notification(device_token)
|
13
|
+
notification = Grocer::PassbookNotification.new(:device_token => device_token)
|
14
|
+
|
15
|
+
pusher.push notification
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|