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.
Files changed (40) hide show
  1. data/.travis.yml +7 -0
  2. data/Gemfile +17 -0
  3. data/Gemfile.lock +65 -0
  4. data/LICENSE +22 -0
  5. data/README.md +290 -0
  6. data/Rakefile +36 -0
  7. data/VERSION +1 -0
  8. data/bin/pk +22 -0
  9. data/lib/commands/build.rb +62 -0
  10. data/lib/commands/commands.rb +31 -0
  11. data/lib/commands/generate.rb +44 -0
  12. data/lib/commands/templates/boarding-pass.json +56 -0
  13. data/lib/commands/templates/coupon.json +33 -0
  14. data/lib/commands/templates/event-ticket.json +33 -0
  15. data/lib/commands/templates/generic.json +33 -0
  16. data/lib/commands/templates/store-card.json +33 -0
  17. data/lib/passbook.rb +15 -0
  18. data/lib/passbook/pkpass.rb +120 -0
  19. data/lib/passbook/push_notification.rb +10 -0
  20. data/lib/passbook/signer.rb +40 -0
  21. data/lib/passbook/version.rb +3 -0
  22. data/lib/rack/passbook_rack.rb +98 -0
  23. data/lib/rails/generators/passbook/config/config_generator.rb +16 -0
  24. data/lib/rails/generators/passbook/config/templates/initializer.rb +13 -0
  25. data/lib/utils/command_utils.rb +12 -0
  26. data/passbook.gemspec +110 -0
  27. data/spec/data/icon.png +0 -0
  28. data/spec/data/icon@2x.png +0 -0
  29. data/spec/data/logo.png +0 -0
  30. data/spec/data/logo@2x.png +0 -0
  31. data/spec/lib/commands/build_spec.rb +92 -0
  32. data/spec/lib/commands/commands_spec.rb +102 -0
  33. data/spec/lib/commands/commands_spec_helper.rb +69 -0
  34. data/spec/lib/commands/generate_spec.rb +72 -0
  35. data/spec/lib/passbook/pkpass_spec.rb +108 -0
  36. data/spec/lib/passbook/push_notification_spec.rb +22 -0
  37. data/spec/lib/passbook/signer_spec.rb +84 -0
  38. data/spec/lib/rack/passbook_rack_spec.rb +233 -0
  39. data/spec/spec_helper.rb +9 -0
  40. 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