passbook-iid 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.travis.yml +7 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +65 -0
- data/LICENSE +22 -0
- data/README.md +290 -0
- data/Rakefile +36 -0
- data/VERSION +1 -0
- data/bin/pk +22 -0
- data/lib/commands/build.rb +62 -0
- data/lib/commands/commands.rb +31 -0
- data/lib/commands/generate.rb +44 -0
- data/lib/commands/templates/boarding-pass.json +56 -0
- data/lib/commands/templates/coupon.json +33 -0
- data/lib/commands/templates/event-ticket.json +33 -0
- data/lib/commands/templates/generic.json +33 -0
- data/lib/commands/templates/store-card.json +33 -0
- data/lib/passbook.rb +15 -0
- data/lib/passbook/pkpass.rb +120 -0
- data/lib/passbook/push_notification.rb +10 -0
- data/lib/passbook/signer.rb +40 -0
- data/lib/passbook/version.rb +3 -0
- data/lib/rack/passbook_rack.rb +98 -0
- data/lib/rails/generators/passbook/config/config_generator.rb +16 -0
- data/lib/rails/generators/passbook/config/templates/initializer.rb +13 -0
- data/lib/utils/command_utils.rb +12 -0
- data/passbook.gemspec +110 -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/commands/build_spec.rb +92 -0
- data/spec/lib/commands/commands_spec.rb +102 -0
- data/spec/lib/commands/commands_spec_helper.rb +69 -0
- data/spec/lib/commands/generate_spec.rb +72 -0
- data/spec/lib/passbook/pkpass_spec.rb +108 -0
- data/spec/lib/passbook/push_notification_spec.rb +22 -0
- data/spec/lib/passbook/signer_spec.rb +84 -0
- data/spec/lib/rack/passbook_rack_spec.rb +233 -0
- data/spec/spec_helper.rb +9 -0
- metadata +216 -0
data/bin/pk
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'commander/import'
|
4
|
+
require 'terminal-table'
|
5
|
+
|
6
|
+
$:.push File.expand_path("../../lib", __FILE__)
|
7
|
+
require 'passbook'
|
8
|
+
require 'utils/command_utils'
|
9
|
+
|
10
|
+
HighLine.track_eof = false # Fix for built-in Ruby
|
11
|
+
Signal.trap("INT") {} # Suppress backtrace when exiting command
|
12
|
+
|
13
|
+
program :version, '0.1'
|
14
|
+
program :description, 'A command-line interface for generating and previewing passbook passes'
|
15
|
+
|
16
|
+
program :help, 'Author', 'Thomas Lauro <>, Lance Gleason <lgleason@polyglotprogramminginc.com>'
|
17
|
+
program :help, 'Website', 'https://github.com/frozon/passbook'
|
18
|
+
program :help_formatter, :compact
|
19
|
+
|
20
|
+
default_command :help
|
21
|
+
|
22
|
+
require 'commands/commands'
|
@@ -0,0 +1,62 @@
|
|
1
|
+
command :build do |c|
|
2
|
+
c.syntax = 'pk build [PASSNAME]'
|
3
|
+
c.summary = 'Creates a .pkpass archive'
|
4
|
+
c.description = ''
|
5
|
+
|
6
|
+
c.example 'description', 'pk archive mypass -o mypass.pkpass'
|
7
|
+
c.option '-w', '--wwdc_certificate /path/to/wwdc_cert.pem', 'Pass certificate'
|
8
|
+
c.option '-k', '--p12_key /path/to/cert.p12'
|
9
|
+
c.option '-c', '--p12_certificate /path/to/cert.p12'
|
10
|
+
c.option '-p', '--password password', 'certificate password'
|
11
|
+
c.option '-o', '--output /path/to/out.pkpass', '.pkpass output filepath'
|
12
|
+
|
13
|
+
c.action do |args, options|
|
14
|
+
determine_directory! unless @directory = args.first
|
15
|
+
validate_directory!
|
16
|
+
|
17
|
+
@filepath = options.output || "#{@directory}.pkpass"
|
18
|
+
validate_output_filepath!
|
19
|
+
|
20
|
+
@certificate = options.wwdc_certificate
|
21
|
+
validate_certificate!
|
22
|
+
|
23
|
+
@password = (options.password ? options.password : (ask("Enter certificate password:"){|q| q.echo = false}))
|
24
|
+
|
25
|
+
Passbook.configure do |passbook|
|
26
|
+
passbook.wwdc_cert = @certificate
|
27
|
+
passbook.p12_key = options.p12_key
|
28
|
+
passbook.p12_certificate = options.p12_certificate
|
29
|
+
passbook.p12_password = @password
|
30
|
+
end
|
31
|
+
|
32
|
+
assets = CommandUtils.get_assets @directory
|
33
|
+
pass_json = File.read(assets.delete(assets.detect{|file| File.basename(file) == 'pass.json'}))
|
34
|
+
pass = Passbook::PKPass.new(pass_json)
|
35
|
+
pass.addFiles assets
|
36
|
+
|
37
|
+
begin
|
38
|
+
pass_stream = pass.stream
|
39
|
+
pass_string = pass_stream.string
|
40
|
+
|
41
|
+
File.open(@filepath, 'w') do |f|
|
42
|
+
f.write pass_string
|
43
|
+
end
|
44
|
+
rescue OpenSSL::PKCS12::PKCS12Error => error
|
45
|
+
say_error "Error: #{error.message}"
|
46
|
+
say_warning "You may be getting this error because the certificate password is either incorrect or missing"
|
47
|
+
abort
|
48
|
+
rescue => error
|
49
|
+
say_error "Error: #{error.message}" and abort
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
alias_command :archive, :build
|
55
|
+
alias_command :b, :build
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def validate_output_filepath!
|
60
|
+
say_error "Filepath required" and abort if @filepath.nil? or @filepath.empty?
|
61
|
+
say_error "#{@filepath} already exists" and abort if File.exist?(@filepath)
|
62
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
$:.push File.expand_path('../', __FILE__)
|
2
|
+
|
3
|
+
require 'commands/build'
|
4
|
+
require 'commands/generate'
|
5
|
+
#require 'commands/serve'
|
6
|
+
# this was added for testability because I couldn't figure out something better.
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def determine_directory!
|
11
|
+
files = Dir['*/pass.json']
|
12
|
+
@directory ||= case files.length
|
13
|
+
when 0 then nil
|
14
|
+
when 1 then File.dirname(files.first)
|
15
|
+
else
|
16
|
+
@directory = choose "Select a directory:", *files.collect{|f| File.dirname(f)}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate_directory!
|
21
|
+
say_error "Missing argument" and abort if @directory.nil?
|
22
|
+
say_error "Directory #{@directory} does not exist" and abort unless File.directory?(@directory)
|
23
|
+
say_error "Directory #{@directory} is not a valid pass" and abort unless File.exist?(File.join(@directory, "pass.json"))
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate_certificate!
|
27
|
+
say_error "Missing or invalid certificate file" and abort if @certificate.nil? or not File.exist?(@certificate)
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
command :generate do |c|
|
4
|
+
c.syntax = 'pk generate PASSNAME'
|
5
|
+
c.summary = 'Generates a template pass directory'
|
6
|
+
c.description = ''
|
7
|
+
|
8
|
+
c.example 'description', 'pk generate mypass'
|
9
|
+
c.option '-T', '--type [boardingPass|coupon|eventTicket|storeCard|generic]', 'Type of pass'
|
10
|
+
|
11
|
+
c.action do |args, options|
|
12
|
+
@directory = args.first
|
13
|
+
@directory ||= ask "Enter a passbook name: "
|
14
|
+
say_error "Missing pass name" and abort if @directory.nil? or @directory.empty?
|
15
|
+
say_error "Directory #{@directory} already exists" and abort if File.directory?(@directory)
|
16
|
+
say_error "File exists at #{@directory}" and abort if File.exist?(@directory)
|
17
|
+
|
18
|
+
@type = options.type
|
19
|
+
determine_type! unless @type
|
20
|
+
validate_type!
|
21
|
+
|
22
|
+
FileUtils.mkdir_p @directory
|
23
|
+
FileUtils.cp File.join(CommandUtils.get_current_directory, '..', 'commands/templates', "#{@type}.json"), File.join(@directory, 'pass.json')
|
24
|
+
['icon.png', 'icon@2x.png'].each do |file|
|
25
|
+
FileUtils.touch File.join(@directory, file)
|
26
|
+
end
|
27
|
+
|
28
|
+
say_ok "Pass generated in #{@directory}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
alias_command :new, :generate
|
33
|
+
alias_command :g, :generate
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def determine_type!
|
38
|
+
@type ||= choose "Select a pass type", *Passbook::PKPass::TYPES
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate_type!
|
42
|
+
say_error %{Invalid type: "#{@type}", expected one of: [#{Passbook::PKPass::TYPES.join(', ')}]} and abort unless Passbook::PKPass::TYPES.include?(@type)
|
43
|
+
end
|
44
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
{
|
2
|
+
"formatVersion" : 1,
|
3
|
+
"passTypeIdentifier" : "pass.com.example.boarding-pass",
|
4
|
+
"description" : "Example Boarding Pass",
|
5
|
+
"teamIdentifier": "Example",
|
6
|
+
"organizationName": "Example",
|
7
|
+
"serialNumber" : "123456",
|
8
|
+
"foregroundColor": "#866B23",
|
9
|
+
"backgroundColor": "#FFD248",
|
10
|
+
"boardingPass" : {
|
11
|
+
"primaryFields" : [
|
12
|
+
{
|
13
|
+
"key" : "origin",
|
14
|
+
"label" : "Atlanta",
|
15
|
+
"value" : "ATL"
|
16
|
+
},
|
17
|
+
{
|
18
|
+
"key" : "destination",
|
19
|
+
"label" : "Johannesburg",
|
20
|
+
"value" : "JNB"
|
21
|
+
}
|
22
|
+
],
|
23
|
+
"secondaryFields" : [
|
24
|
+
{
|
25
|
+
"key" : "boarding-gate",
|
26
|
+
"label" : "Gate",
|
27
|
+
"value" : "F12"
|
28
|
+
}
|
29
|
+
],
|
30
|
+
"auxiliaryFields" : [
|
31
|
+
{
|
32
|
+
"key" : "seat",
|
33
|
+
"label" : "Seat",
|
34
|
+
"value" : "7A"
|
35
|
+
},
|
36
|
+
{
|
37
|
+
"key" : "passenger-name",
|
38
|
+
"label" : "Passenger",
|
39
|
+
"value" : "Honey Badger"
|
40
|
+
}
|
41
|
+
],
|
42
|
+
"transitType" : "PKTransitTypeAir",
|
43
|
+
"barcode" : {
|
44
|
+
"message" : "DL123",
|
45
|
+
"format" : "PKBarcodeFormatQR",
|
46
|
+
"messageEncoding" : "iso-8859-1"
|
47
|
+
},
|
48
|
+
"backFields" : [
|
49
|
+
{
|
50
|
+
"key" : "terms",
|
51
|
+
"label" : "Terms and Conditions",
|
52
|
+
"value" : "Valid for date of travel only"
|
53
|
+
}
|
54
|
+
]
|
55
|
+
}
|
56
|
+
}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
{
|
2
|
+
"formatVersion" : 1,
|
3
|
+
"passTypeIdentifier" : "pass.com.example.coupon",
|
4
|
+
"description" : "Example Coupon",
|
5
|
+
"teamIdentifier": "Example",
|
6
|
+
"organizationName": "Example",
|
7
|
+
"serialNumber" : "123456",
|
8
|
+
"foregroundColor": "#FFFFFF",
|
9
|
+
"backgroundColor": "#C799FF",
|
10
|
+
"generic" : {
|
11
|
+
"primaryFields" : [
|
12
|
+
|
13
|
+
],
|
14
|
+
"secondaryFields" : [
|
15
|
+
|
16
|
+
],
|
17
|
+
"auxiliaryFields" : [
|
18
|
+
|
19
|
+
],
|
20
|
+
"barcode" : {
|
21
|
+
"message" : "ABCD 123 EFGH 456 IJKL 789 MNOP",
|
22
|
+
"format" : "PKBarcodeFormatPDF417",
|
23
|
+
"messageEncoding" : "iso-8859-1"
|
24
|
+
},
|
25
|
+
"backFields" : [
|
26
|
+
{
|
27
|
+
"key" : "terms",
|
28
|
+
"label" : "Terms and Conditions",
|
29
|
+
"value" : "T's and C's Apply"
|
30
|
+
}
|
31
|
+
]
|
32
|
+
}
|
33
|
+
}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
{
|
2
|
+
"formatVersion" : 1,
|
3
|
+
"passTypeIdentifier" : "pass.com.example.event-ticket",
|
4
|
+
"description" : "Example Event Ticket",
|
5
|
+
"teamIdentifier": "Example",
|
6
|
+
"organizationName": "Example",
|
7
|
+
"serialNumber" : "123456",
|
8
|
+
"foregroundColor": "#FFFFFF",
|
9
|
+
"backgroundColor": "#FF5453",
|
10
|
+
"generic" : {
|
11
|
+
"primaryFields" : [
|
12
|
+
|
13
|
+
],
|
14
|
+
"secondaryFields" : [
|
15
|
+
|
16
|
+
],
|
17
|
+
"auxiliaryFields" : [
|
18
|
+
|
19
|
+
],
|
20
|
+
"barcode" : {
|
21
|
+
"message" : "ABCD 123 EFGH 456 IJKL 789 MNOP",
|
22
|
+
"format" : "PKBarcodeFormatPDF417",
|
23
|
+
"messageEncoding" : "iso-8859-1"
|
24
|
+
},
|
25
|
+
"backFields" : [
|
26
|
+
{
|
27
|
+
"key" : "terms",
|
28
|
+
"label" : "Terms and Conditions",
|
29
|
+
"value" : "T's and C's apply"
|
30
|
+
}
|
31
|
+
]
|
32
|
+
}
|
33
|
+
}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
{
|
2
|
+
"formatVersion" : 1,
|
3
|
+
"passTypeIdentifier" : "pass.com.example.generic",
|
4
|
+
"description" : "Example Generic Pass",
|
5
|
+
"teamIdentifier": "Example",
|
6
|
+
"organizationName": "Example",
|
7
|
+
"serialNumber" : "123456",
|
8
|
+
"foregroundColor": "#FFFFFF",
|
9
|
+
"backgroundColor": "#444444",
|
10
|
+
"generic" : {
|
11
|
+
"primaryFields" : [
|
12
|
+
|
13
|
+
],
|
14
|
+
"secondaryFields" : [
|
15
|
+
|
16
|
+
],
|
17
|
+
"auxiliaryFields" : [
|
18
|
+
|
19
|
+
],
|
20
|
+
"barcode" : {
|
21
|
+
"message" : "ABCD 123 EFGH 456 IJKL 789 MNOP",
|
22
|
+
"format" : "PKBarcodeFormatPDF417",
|
23
|
+
"messageEncoding" : "iso-8859-1"
|
24
|
+
},
|
25
|
+
"backFields" : [
|
26
|
+
{
|
27
|
+
"key" : "terms",
|
28
|
+
"label" : "Terms and Conditions",
|
29
|
+
"value" : "Put your terms here"
|
30
|
+
}
|
31
|
+
]
|
32
|
+
}
|
33
|
+
}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
{
|
2
|
+
"formatVersion" : 1,
|
3
|
+
"passTypeIdentifier" : "pass.com.example.store-card",
|
4
|
+
"description" : "Example Store Card",
|
5
|
+
"teamIdentifier": "Example",
|
6
|
+
"organizationName": "Example",
|
7
|
+
"serialNumber" : "123456",
|
8
|
+
"foregroundColor": "#FFFFFF",
|
9
|
+
"backgroundColor": "#AFC1E3",
|
10
|
+
"generic" : {
|
11
|
+
"primaryFields" : [
|
12
|
+
|
13
|
+
],
|
14
|
+
"secondaryFields" : [
|
15
|
+
|
16
|
+
],
|
17
|
+
"auxiliaryFields" : [
|
18
|
+
|
19
|
+
],
|
20
|
+
"barcode" : {
|
21
|
+
"message" : "ABCD 123 EFGH 456 IJKL 789 MNOP",
|
22
|
+
"format" : "PKBarcodeFormatPDF417",
|
23
|
+
"messageEncoding" : "iso-8859-1"
|
24
|
+
},
|
25
|
+
"backFields" : [
|
26
|
+
{
|
27
|
+
"key" : "terms",
|
28
|
+
"label" : "Terms and Conditions",
|
29
|
+
"value" : "T's and C's apply"
|
30
|
+
}
|
31
|
+
]
|
32
|
+
}
|
33
|
+
}
|
data/lib/passbook.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "passbook/version"
|
2
|
+
require "passbook/pkpass"
|
3
|
+
require "passbook/signer"
|
4
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
5
|
+
require 'passbook/push_notification'
|
6
|
+
require 'grocer/passbook_notification'
|
7
|
+
require 'rack/passbook_rack'
|
8
|
+
|
9
|
+
module Passbook
|
10
|
+
mattr_accessor :p12_certificate, :p12_password, :wwdc_cert, :p12_key, :notification_cert, :notification_gateway
|
11
|
+
|
12
|
+
def self.configure
|
13
|
+
yield self
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,120 @@
|
|
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 = ['boarding-pass', 'coupon', 'event-ticket', 'store-card', 'generic']
|
11
|
+
|
12
|
+
def initialize pass, init_signer = nil
|
13
|
+
@pass = pass
|
14
|
+
@manifest_files = []
|
15
|
+
@signer = init_signer || Passbook::Signer.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def addFile file
|
19
|
+
@manifest_files << file
|
20
|
+
end
|
21
|
+
|
22
|
+
def addFiles files
|
23
|
+
@manifest_files += files
|
24
|
+
end
|
25
|
+
|
26
|
+
# for backwards compatibility
|
27
|
+
def json= json
|
28
|
+
@pass = json
|
29
|
+
end
|
30
|
+
|
31
|
+
def build
|
32
|
+
manifest = createManifest
|
33
|
+
|
34
|
+
# Check pass for necessary files and fields
|
35
|
+
checkPass manifest
|
36
|
+
|
37
|
+
# Create pass signature
|
38
|
+
signature = @signer.sign manifest
|
39
|
+
|
40
|
+
return [manifest, signature]
|
41
|
+
end
|
42
|
+
|
43
|
+
# Backward compatibility
|
44
|
+
def create
|
45
|
+
self.file.path
|
46
|
+
end
|
47
|
+
|
48
|
+
# Return a Tempfile containing our ZipStream
|
49
|
+
def file(options = {})
|
50
|
+
options[:file_name] ||= 'pass.pkpass'
|
51
|
+
|
52
|
+
temp_file = Tempfile.new(options[:file_name])
|
53
|
+
temp_file.write self.stream.string
|
54
|
+
temp_file.close
|
55
|
+
|
56
|
+
temp_file
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return a ZipOutputStream
|
60
|
+
def stream
|
61
|
+
manifest, signature = build
|
62
|
+
|
63
|
+
outputZip manifest, signature
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def checkPass manifest
|
69
|
+
# Check for default images
|
70
|
+
raise 'Icon missing' unless manifest.include?('icon.png')
|
71
|
+
raise 'Icon@2x missing' unless manifest.include?('icon@2x.png')
|
72
|
+
|
73
|
+
# Check for developer field in JSON
|
74
|
+
raise 'Pass Type Identifier missing' unless @pass.include?('passTypeIdentifier')
|
75
|
+
raise 'Team Identifier missing' unless @pass.include?('teamIdentifier')
|
76
|
+
raise 'Serial Number missing' unless @pass.include?('serialNumber')
|
77
|
+
raise 'Organization Name Identifier missing' unless @pass.include?('organizationName')
|
78
|
+
raise 'Format Version' unless @pass.include?('formatVersion')
|
79
|
+
raise 'Format Version should be a numeric' unless JSON.parse(@pass)['formatVersion'].is_a?(Numeric)
|
80
|
+
raise 'Description' unless @pass.include?('description')
|
81
|
+
end
|
82
|
+
|
83
|
+
def createManifest
|
84
|
+
sha1s = {}
|
85
|
+
sha1s['pass.json'] = Digest::SHA1.hexdigest @pass
|
86
|
+
|
87
|
+
@manifest_files.each do |file|
|
88
|
+
if file.class == Hash
|
89
|
+
sha1s[file[:name]] = Digest::SHA1.hexdigest file[:content]
|
90
|
+
else
|
91
|
+
sha1s[File.basename(file)] = Digest::SHA1.file(file).hexdigest
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
return sha1s.to_json
|
96
|
+
end
|
97
|
+
|
98
|
+
def outputZip manifest, signature
|
99
|
+
|
100
|
+
Zip::OutputStream.write_buffer do |zip|
|
101
|
+
zip.put_next_entry 'pass.json'
|
102
|
+
zip.write @pass
|
103
|
+
zip.put_next_entry 'manifest.json'
|
104
|
+
zip.write manifest
|
105
|
+
zip.put_next_entry 'signature'
|
106
|
+
zip.write signature
|
107
|
+
|
108
|
+
@manifest_files.each do |file|
|
109
|
+
if file.class == Hash
|
110
|
+
zip.put_next_entry file[:name]
|
111
|
+
zip.print file[:content]
|
112
|
+
else
|
113
|
+
zip.put_next_entry File.basename(file)
|
114
|
+
zip.print IO.read(file)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|