passbook-iid 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|