passbook-iid 0.4.0

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