passkit 0.1.0 → 0.2.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +121 -5
  3. data/app/assets/images/passkit/add_to_apple_wallet_de.png +0 -0
  4. data/app/assets/images/passkit/add_to_apple_wallet_en.png +0 -0
  5. data/app/assets/images/passkit/add_to_apple_wallet_fr.png +0 -0
  6. data/app/assets/images/passkit/add_to_apple_wallet_it.png +0 -0
  7. data/app/assets/images/passkit/add_to_walletpass_de.png +0 -0
  8. data/app/assets/images/passkit/add_to_walletpass_en.png +0 -0
  9. data/app/assets/images/passkit/add_to_walletpass_fr.png +0 -0
  10. data/app/assets/images/passkit/add_to_walletpass_it.png +0 -0
  11. data/app/controllers/passkit/api/v1/logs_controller.rb +14 -0
  12. data/app/controllers/passkit/api/v1/passes_controller.rb +55 -0
  13. data/app/controllers/passkit/api/v1/registrations_controller.rb +115 -0
  14. data/app/controllers/passkit/logs_controller.rb +9 -0
  15. data/app/controllers/passkit/passes_controller.rb +4 -19
  16. data/app/controllers/passkit/previews_controller.rb +13 -0
  17. data/app/mailers/passkit/example_mailer.rb +8 -0
  18. data/{lib → app/models}/passkit/device.rb +2 -2
  19. data/{lib → app/models}/passkit/log.rb +0 -0
  20. data/app/models/passkit/pass.rb +45 -0
  21. data/app/models/passkit/registration.rb +6 -0
  22. data/app/views/layouts/passkit/application.html.erb +16 -0
  23. data/app/views/passkit/example_mailer/example_email.html.erb +15 -0
  24. data/app/views/passkit/logs/index.html.erb +22 -0
  25. data/app/views/passkit/passes/index.html.erb +32 -0
  26. data/app/views/passkit/previews/index.html.erb +21 -0
  27. data/app/views/shared/passkit/_navigation.html.erb +5 -0
  28. data/config/routes.rb +17 -7
  29. data/docs/wallet.png +0 -0
  30. data/lib/generators/passkit/install_generator.rb +26 -0
  31. data/lib/generators/templates/create_passkit_tables.rb.tt +32 -0
  32. data/lib/generators/templates/passkit.rb +3 -0
  33. data/lib/passkit/base_pass.rb +120 -0
  34. data/lib/passkit/example_store_card/icon.png +0 -0
  35. data/lib/passkit/example_store_card/icon@2x.png +0 -0
  36. data/lib/passkit/example_store_card/icon@3x.png +0 -0
  37. data/lib/passkit/example_store_card/logo.png +0 -0
  38. data/lib/passkit/example_store_card.rb +110 -0
  39. data/lib/passkit/factory.rb +2 -2
  40. data/lib/passkit/generator.rb +30 -22
  41. data/lib/passkit/url_encrypt.rb +3 -3
  42. data/lib/passkit/url_generator.rb +27 -0
  43. data/lib/passkit/version.rb +1 -1
  44. data/lib/passkit.rb +36 -3
  45. data/sig/lib/passkit/encryptable_payload.rbs +5 -0
  46. data/sig/lib/passkit/factory.rbs +5 -0
  47. data/sig/lib/passkit/generator.rbs +8 -0
  48. data/sig/lib/passkit/generator_object.rbs +5 -0
  49. data/sig/lib/passkit/pass.rbs +5 -0
  50. data/sig/lib/passkit/url_encrypt.rbs +6 -0
  51. data/sig/lib/passkit/url_generator.rbs +9 -0
  52. metadata +116 -8
  53. data/lib/passkit/pass.rb +0 -8
  54. data/sig/passkit.rbs +0 -4
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+ require "rails/generators/migration"
5
+
6
+ module Passkit
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("../../templates", __FILE__)
12
+
13
+ # Implement the required interface for Rails::Generators::Migration.
14
+ def self.next_migration_number(dirname)
15
+ next_migration_number = current_migration_number(dirname) + 1
16
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
17
+ end
18
+
19
+ desc "Copy all files to your application."
20
+ def generate_files
21
+ migration_template "create_passkit_tables.rb", "db/migrate/create_passkit_tables.rb"
22
+ copy_file "passkit.rb", "config/initializers/passkit.rb"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ class CreatePasskitTables < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :passkit_passes do |t|
4
+ t.string :generator_type
5
+ t.string :klass
6
+ t.bigint :generator_id
7
+ t.string :serial_number
8
+ t.string :authentication_token
9
+ t.json :data
10
+ t.integer :version
11
+ t.timestamps null: false
12
+ t.index [:generator_type, :generator_id], name: 'index_passkit_passes_on_generator'
13
+ end
14
+
15
+ create_table :passkit_devices do |t|
16
+ t.string :identifier
17
+ t.string :push_token
18
+ t.timestamps null: false
19
+ end
20
+
21
+ create_table :passkit_registrations do |t|
22
+ t.belongs_to :passkit_pass, index: true
23
+ t.belongs_to :passkit_device, index: true
24
+ t.timestamps null: false
25
+ end
26
+
27
+ create_table :passkit_logs do |t|
28
+ t.text :content
29
+ t.timestamps null: false
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ Passkit.configure do |config|
2
+ # config.available_passes['Passkit::YourPass'] = -> { User.create }
3
+ end
@@ -0,0 +1,120 @@
1
+ module Passkit
2
+ class BasePass
3
+ def initialize(generator = nil)
4
+ @generator = generator
5
+ end
6
+
7
+ def format_version
8
+ ENV["PASSKIT_FORMAT_VERSION"] || 1
9
+ end
10
+
11
+ def apple_team_identifier
12
+ ENV["PASSKIT_APPLE_TEAM_IDENTIFIER"] || raise(Error.new("Missing environment variable: PASSKIT_APPLE_TEAM_IDENTIFIER"))
13
+ end
14
+
15
+ def pass_type_identifier
16
+ ENV["PASSKIT_PASS_TYPE_IDENTIFIER"] || raise(Error.new("Missing environment variable: PASSKIT_PASS_TYPE_IDENTIFIER"))
17
+ end
18
+
19
+ def language
20
+ nil
21
+ end
22
+
23
+ def pass_path
24
+ rails_folder = Rails.root.join("private/passkit/#{folder_name}")
25
+ # if folder exists, otherwise is in the gem itself under lib/passkit/base_pass
26
+ if File.directory?(rails_folder)
27
+ rails_folder
28
+ else
29
+ File.join(File.dirname(__FILE__), folder_name)
30
+ end
31
+ end
32
+
33
+ def pass_type
34
+ :storeCard
35
+ # :coupon
36
+ end
37
+
38
+ def web_service_url
39
+ raise Error.new("Missing environment variable: PASSKIT_WEB_SERVICE_HOST") unless ENV["PASSKIT_WEB_SERVICE_HOST"]
40
+ "#{ENV["PASSKIT_WEB_SERVICE_HOST"]}/passkit/api"
41
+ end
42
+
43
+ def foreground_color
44
+ "rgb(0, 0, 0)"
45
+ end
46
+
47
+ def background_color
48
+ "rgb(255, 255, 255)"
49
+ end
50
+
51
+ def organization_name
52
+ "Passkit"
53
+ end
54
+
55
+ def description
56
+ "A basic description for a pass"
57
+ end
58
+
59
+ # A pass can have up to ten relevant locations
60
+ #
61
+ # @see https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/Creating.html
62
+ def locations
63
+ []
64
+ end
65
+
66
+ def voided
67
+ false
68
+ end
69
+
70
+ def file_name
71
+ @file_name ||= SecureRandom.uuid
72
+ end
73
+
74
+ # QRCode by default
75
+ def barcode
76
+ {messageEncoding: "iso-8859-1",
77
+ format: "PKBarcodeFormatQR",
78
+ message: "https://github.com/coorasse/passkit",
79
+ altText: "https://github.com/coorasse/passkit"}
80
+ end
81
+
82
+ # Barcode example
83
+ # def barcode
84
+ # { messageEncoding: 'iso-8859-1',
85
+ # format: 'PKBarcodeFormatCode128',
86
+ # message: '12345',
87
+ # altText: '12345' }
88
+ # end
89
+
90
+ def logo_text
91
+ "Logo text"
92
+ end
93
+
94
+ def header_fields
95
+ []
96
+ end
97
+
98
+ def primary_fields
99
+ []
100
+ end
101
+
102
+ def secondary_fields
103
+ []
104
+ end
105
+
106
+ def auxiliary_fields
107
+ []
108
+ end
109
+
110
+ def back_fields
111
+ []
112
+ end
113
+
114
+ private
115
+
116
+ def folder_name
117
+ self.class.name.demodulize.underscore
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,110 @@
1
+ module Passkit
2
+ class ExampleStoreCard < BasePass
3
+ def pass_type
4
+ :storeCard
5
+ # :coupon
6
+ end
7
+
8
+ def foreground_color
9
+ "rgb(0, 0, 0)"
10
+ end
11
+
12
+ def background_color
13
+ "rgb(255, 255, 255)"
14
+ end
15
+
16
+ def organization_name
17
+ "Passkit"
18
+ end
19
+
20
+ def description
21
+ "A basic description for a pass"
22
+ end
23
+
24
+ # A pass can have up to ten relevant locations
25
+ #
26
+ # @see https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/Creating.html
27
+ def locations
28
+ []
29
+ end
30
+
31
+ def voided
32
+ false
33
+ end
34
+
35
+ def file_name
36
+ @file_name ||= SecureRandom.uuid
37
+ end
38
+
39
+ # QRCode by default
40
+ def barcode
41
+ {messageEncoding: "iso-8859-1",
42
+ format: "PKBarcodeFormatQR",
43
+ message: "https://github.com/coorasse/passkit",
44
+ altText: "https://github.com/coorasse/passkit"}
45
+ end
46
+
47
+ # Barcode example
48
+ # def barcode
49
+ # { messageEncoding: 'iso-8859-1',
50
+ # format: 'PKBarcodeFormatCode128',
51
+ # message: '12345',
52
+ # altText: '12345' }
53
+ # end
54
+
55
+ def logo_text
56
+ "Loyalty Card"
57
+ end
58
+
59
+ def header_fields
60
+ [{
61
+ key: "balance",
62
+ label: "Balance",
63
+ value: 100,
64
+ currencyCode: "$"
65
+ }]
66
+ end
67
+
68
+ def back_fields
69
+ [{
70
+ key: "example1",
71
+ label: "Code",
72
+ value: "0123456789"
73
+ },
74
+ {
75
+ key: "example2",
76
+ label: "Creator",
77
+ value: "https://github.com/coorasse"
78
+ },
79
+ {
80
+ key: "example3",
81
+ label: "Contact",
82
+ value: "rodi@hey.com"
83
+ }]
84
+ end
85
+
86
+ def auxiliary_fields
87
+ [{
88
+ key: "name",
89
+ label: "Name",
90
+ value: "Alessandro Rodi"
91
+ },
92
+ {
93
+ key: "email",
94
+ label: "Email",
95
+ value: "rodi@hey.com"
96
+ },
97
+ {
98
+ key: "phone",
99
+ label: "Phone",
100
+ value: "+41 1234567890"
101
+ }]
102
+ end
103
+
104
+ private
105
+
106
+ def folder_name
107
+ self.class.name.demodulize.underscore
108
+ end
109
+ end
110
+ end
@@ -1,8 +1,8 @@
1
1
  module Passkit
2
2
  class Factory
3
3
  class << self
4
- def create_pass(generator)
5
- pass = Pass.create!(generator: generator)
4
+ def create_pass(generator, pass_class)
5
+ pass = Pass.create!(generator: generator, klass: pass_class)
6
6
  Passkit::Generator.new(pass).generate_and_sign
7
7
  end
8
8
  end
@@ -1,13 +1,16 @@
1
+ require "zip"
2
+
1
3
  module Passkit
2
4
  class Generator
3
- TMP_FOLDER = Rails.root.join('tmp/pass_kit').freeze
5
+ TMP_FOLDER = Rails.root.join("tmp/passkit").freeze
4
6
 
5
- def initialize(pass_name, pass)
7
+ def initialize(pass)
6
8
  @pass = pass
7
9
  @generator = pass.generator
8
10
  end
9
11
 
10
12
  def generate_and_sign
13
+ check_necessary_files
11
14
  create_temporary_directory
12
15
  copy_pass_to_tmp_location
13
16
  clean_ds_store_files
@@ -21,9 +24,13 @@ module Passkit
21
24
 
22
25
  private
23
26
 
27
+ def check_necessary_files
28
+ raise "icon.png is not present in #{@pass.pass_path}" unless File.exist?(File.join(@pass.pass_path, "icon.png"))
29
+ end
30
+
24
31
  def create_temporary_directory
25
32
  FileUtils.mkdir_p(TMP_FOLDER) unless File.directory?(TMP_FOLDER)
26
- @temporary_path = TMP_FOLDER.join(@pass.file_name)
33
+ @temporary_path = TMP_FOLDER.join(@pass.file_name.to_s)
27
34
 
28
35
  FileUtils.rm_rf(@temporary_path) if File.directory?(@temporary_path)
29
36
  end
@@ -33,10 +40,9 @@ module Passkit
33
40
  end
34
41
 
35
42
  def clean_ds_store_files
36
- Dir.glob(@temporary_path.join('**/.DS_Store')).each { |file| File.delete(file) }
43
+ Dir.glob(@temporary_path.join("**/.DS_Store")).each { |file| File.delete(file) }
37
44
  end
38
45
 
39
- # rubocop:disable Metrics/AbcSize
40
46
  def generate_json_pass
41
47
  pass = {
42
48
  formatVersion: @pass.format_version,
@@ -53,35 +59,37 @@ module Passkit
53
59
  }
54
60
 
55
61
  pass = pass.merge({
56
- serialNumber: @wallet_pass.serial_number,
57
- passTypeIdentifier: @wallet_pass.pass_type_identifier,
58
- authenticationToken: @wallet_pass.authentication_token
59
- })
62
+ serialNumber: @pass.serial_number,
63
+ passTypeIdentifier: @pass.pass_type_identifier,
64
+ authenticationToken: @pass.authentication_token
65
+ })
60
66
 
61
67
  pass[@pass.pass_type] = {
62
68
  headerFields: @pass.header_fields,
69
+ primaryFields: @pass.primary_fields,
70
+ secondaryFields: @pass.secondary_fields,
63
71
  auxiliaryFields: @pass.auxiliary_fields,
64
72
  backFields: @pass.back_fields
65
73
  }
66
74
 
67
- File.open(@temporary_path.join('pass.json'), 'w') { |f| f.write(pass.to_json) }
75
+ File.write(@temporary_path.join("pass.json"), pass.to_json)
68
76
  end
69
77
 
70
78
  # rubocop:enable Metrics/AbcSize
71
79
 
72
80
  def generate_json_manifest
73
81
  manifest = {}
74
- Dir.glob(@temporary_path.join('**')).each do |file|
82
+ Dir.glob(@temporary_path.join("**")).each do |file|
75
83
  manifest[File.basename(file)] = Digest::SHA1.hexdigest(File.read(file))
76
84
  end
77
85
 
78
- @manifest_url = @temporary_path.join('manifest.json')
79
- File.open(@manifest_url, 'w') { |f| f.write(manifest.to_json) }
86
+ @manifest_url = @temporary_path.join("manifest.json")
87
+ File.write(@manifest_url, manifest.to_json)
80
88
  end
81
89
 
82
- CERTIFICATE = Rails.root.join(ENV['PASSKIT_PRIVATE_P12_CERTIFICATE'])
83
- INTERMEDIATE_CERTIFICATE = Rails.root.join(ENV['APPLE_INTERMEDIATE_CERTIFICATE'])
84
- CERTIFICATE_PASSWORD = ENV['PASSKIT_CERTIFICATE_KEY']
90
+ CERTIFICATE = Rails.root.join(ENV["PASSKIT_PRIVATE_P12_CERTIFICATE"])
91
+ INTERMEDIATE_CERTIFICATE = Rails.root.join(ENV["PASSKIT_APPLE_INTERMEDIATE_CERTIFICATE"])
92
+ CERTIFICATE_PASSWORD = ENV["PASSKIT_CERTIFICATE_KEY"]
85
93
 
86
94
  # :nocov:
87
95
  def sign_manifest
@@ -90,21 +98,21 @@ module Passkit
90
98
 
91
99
  flag = OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY
92
100
  signed = OpenSSL::PKCS7.sign(p12_certificate.certificate,
93
- p12_certificate.key, File.read(@manifest_url),
94
- [intermediate_certificate], flag)
101
+ p12_certificate.key, File.read(@manifest_url),
102
+ [intermediate_certificate], flag)
95
103
 
96
- @signature_url = @temporary_path.join('signature')
97
- File.open(@signature_url, 'w') { |f| f.syswrite signed.to_der }
104
+ @signature_url = @temporary_path.join("signature")
105
+ File.open(@signature_url, "w") { |f| f.syswrite signed.to_der }
98
106
  end
99
107
 
100
108
  # :nocov:
101
109
 
102
110
  def compress_pass_file
103
111
  zip_path = TMP_FOLDER.join("#{@pass.file_name}.pkpass")
104
- zipped_file = File.open(zip_path, 'w')
112
+ zipped_file = File.open(zip_path, "w")
105
113
 
106
114
  Zip::OutputStream.open(zipped_file.path) do |z|
107
- Dir.glob(@temporary_path.join('**')).each do |file|
115
+ Dir.glob(@temporary_path.join("**")).each do |file|
108
116
  z.put_next_entry(File.basename(file))
109
117
  z.print File.read(file)
110
118
  end
@@ -7,13 +7,13 @@ module Passkit
7
7
  cipher.key = encryption_key
8
8
  s = cipher.update(string) + cipher.final
9
9
 
10
- s.unpack1('H*').upcase
10
+ s.unpack1("H*").upcase
11
11
  end
12
12
 
13
13
  def decrypt(string)
14
14
  cipher = cypher.decrypt
15
15
  cipher.key = encryption_key
16
- s = [string].pack('H*').unpack('C*').pack('c*')
16
+ s = [string].pack("H*").unpack("C*").pack("c*")
17
17
 
18
18
  JSON.parse(cipher.update(s) + cipher.final, symbolize_names: true)
19
19
  end
@@ -25,7 +25,7 @@ module Passkit
25
25
  end
26
26
 
27
27
  def cypher
28
- OpenSSL::Cipher.new('AES-128-CBC')
28
+ OpenSSL::Cipher.new("AES-128-CBC")
29
29
  end
30
30
  end
31
31
  end
@@ -0,0 +1,27 @@
1
+ module Passkit
2
+ class UrlGenerator
3
+ include Passkit::Engine.routes.url_helpers
4
+
5
+ VALIDITY = 30.days
6
+
7
+ def initialize(pass_class, generator = nil)
8
+ valid_until = VALIDITY.from_now
9
+
10
+ payload = {valid_until: valid_until,
11
+ generator_class: generator&.class&.name,
12
+ generator_id: generator&.id,
13
+ pass_class: pass_class.name}
14
+ @url = passes_api_url(host: ENV["PASSKIT_WEB_SERVICE_HOST"], payload: UrlEncrypt.encrypt(payload))
15
+ end
16
+
17
+ def ios
18
+ @url
19
+ end
20
+
21
+ WALLET_PASS_PREFIX = "https://walletpass.io?u=".freeze
22
+ # @see https://walletpasses.io/developer/
23
+ def android
24
+ "#{WALLET_PASS_PREFIX}#{@url}"
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Passkit
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/passkit.rb CHANGED
@@ -1,9 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "passkit/version"
4
- require 'passkit/engine'
3
+ require "passkit/engine"
4
+
5
+ require "zeitwerk"
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.ignore("#{__dir__}/generators")
8
+ loader.setup
5
9
 
6
10
  module Passkit
7
11
  class Error < StandardError; end
8
- # Your code goes here...
12
+
13
+ class << self
14
+ attr_accessor :configuration
15
+ end
16
+
17
+ def self.configure
18
+ self.configuration ||= Configuration.new
19
+ yield(configuration)
20
+ end
21
+
22
+ class Configuration
23
+ attr_accessor :available_passes,
24
+ :web_service_host,
25
+ :certificate_key,
26
+ :private_p12_certificate,
27
+ :apple_intermediate_certificate,
28
+ :apple_team_identifier,
29
+ :pass_type_identifier
30
+
31
+ def initialize
32
+ @available_passes = {"Passkit::ExampleStoreCard" => -> {}}
33
+ @web_service_host = ENV["PASSKIT_WEB_SERVICE_HOST"] || (raise "Please set PASSKIT_WEB_SERVICE_HOST")
34
+ raise("PASSKIT_WEB_SERVICE_HOST must start with https://") unless @web_service_host.starts_with?("https://")
35
+ @certificate_key = ENV["PASSKIT_CERTIFICATE_KEY"] || (raise "Please set PASSKIT_CERTIFICATE_KEY")
36
+ @private_p12_certificate = ENV["PASSKIT_PRIVATE_P12_CERTIFICATE"] || (raise "Please set PASSKIT_PRIVATE_P12_CERTIFICATE")
37
+ @apple_intermediate_certificate = ENV["PASSKIT_APPLE_INTERMEDIATE_CERTIFICATE"] || (raise "Please set PASSKIT_APPLE_INTERMEDIATE_CERTIFICATE")
38
+ @apple_team_identifier = ENV["PASSKIT_APPLE_TEAM_IDENTIFIER"] || (raise "Please set PASSKIT_APPLE_TEAM_IDENTIFIER")
39
+ @pass_type_identifier = ENV["PASSKIT_PASS_TYPE_IDENTIFIER"] || (raise "Please set PASSKIT_PASS_TYPE_IDENTIFIER")
40
+ end
41
+ end
9
42
  end
@@ -0,0 +1,5 @@
1
+ module Passkit
2
+ class EncryptablePayload
3
+ def to_json: () -> Hash[Symbol, String]
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Passkit
2
+ class Factory
3
+ def self.create_pass: (Passkit::GeneratorObject generator, String pass_class) -> String
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ module Passkit
2
+ class Generator
3
+ TMP_FOLDER: String
4
+
5
+ def initialize: (Passkit::Pass pass) -> void
6
+ def generate_and_sign: () -> String
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ module Passkit
2
+ class GeneratorObject
3
+ def id: () -> String
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Passkit
2
+ class Pass
3
+ def instance: () -> GeneratorObject
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module Passkit
2
+ class UrlEncrypt
3
+ def self.encrypt: (Hash[untyped, untyped] payload) -> String
4
+ def self.decrypt: (String string) -> Hash[Symbol, untyped]
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module Passkit
2
+ class UrlGenerator
3
+ def initialize: (Class passClass, GeneratorObject ?generator) -> void
4
+ def ios: () -> String
5
+
6
+ # @see https://walletpasses.io/developer/
7
+ def android: () -> String
8
+ end
9
+ end