wallet_passkit 0.1.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.
- checksums.yaml +7 -0
- data/README.md +110 -0
- data/lib/wallet_passkit/apple/service.rb +82 -0
- data/lib/wallet_passkit/apple/signer.rb +23 -0
- data/lib/wallet_passkit/apple.rb +4 -0
- data/lib/wallet_passkit/configuration.rb +25 -0
- data/lib/wallet_passkit/error.rb +3 -0
- data/lib/wallet_passkit/google/README.md +136 -0
- data/lib/wallet_passkit/google/auth.rb +18 -0
- data/lib/wallet_passkit/google/pass.rb +198 -0
- data/lib/wallet_passkit/google/pass_updater.rb +95 -0
- data/lib/wallet_passkit/google.rb +5 -0
- data/lib/wallet_passkit/railtie.rb +30 -0
- data/lib/wallet_passkit/version.rb +5 -0
- data/lib/wallet_passkit.rb +26 -0
- metadata +141 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2ca6dbbeb4df4946ae410fadb3c040d8770e9a9ba1db838d90d151e805d98ab0
|
4
|
+
data.tar.gz: 83efa527ed456b51131a8a0bcc15c870bb618a4837bebbd2c4dde72cb089e32d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a436ab2fe7597f2740b58eb996090641ee66134b22d0bf64829928696a5bf76442dd8a22e90a701545f6a41ef36505347753b5d71a4db988c8601a74f3b9fa54
|
7
|
+
data.tar.gz: e6c5eb667e70f6e638e449b6c50bad4c22d1a8d60e2020d682d9066e780e89e6ace194076d174841848471a3cad61ff0d908d6413f1574b1bbfac908cfd52a90
|
data/README.md
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# wallet_passkit
|
2
|
+
|
3
|
+
A modern Ruby gem to generate Apple Wallet passes (.pkpass) and integrate easily into Rails projects. Also includes Google Wallet helpers (Save link, object updates).
|
4
|
+
|
5
|
+
[](https://github.com/gioggi/wallet_passkit/actions/workflows/test.yml)
|
6
|
+
|
7
|
+
Status: initial implementation for Apple + Google (Save link + updater).
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'wallet_passkit', path: '.' # or gem 'wallet_passkit', version: '0.1.0'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
bundle install
|
21
|
+
```
|
22
|
+
|
23
|
+
## Configuration
|
24
|
+
|
25
|
+
Global configuration in plain Ruby:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require 'wallet_passkit'
|
29
|
+
|
30
|
+
WalletPasskit.configure do |c|
|
31
|
+
c.apple_pass_certificate_p12_path = "/path/to/pass_certificate.p12"
|
32
|
+
c.apple_pass_certificate_password = ENV["APPLE_PASS_P12_PASSWORD"]
|
33
|
+
c.apple_wwdr_certificate_path = "/path/to/AppleWWDRCAG3.pem" # download from Apple
|
34
|
+
c.apple_team_identifier = "TEAMID1234"
|
35
|
+
c.apple_organization_name = "My Org"
|
36
|
+
|
37
|
+
# Google Wallet (optional for Save link / updater)
|
38
|
+
c.google_service_account_credentials = "/path/to/service_account.json"
|
39
|
+
c.google_issuer_id = "issuer-id" # optional
|
40
|
+
c.google_class_prefix = "com.example" # optional
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
Rails (config/initializers/wallet_passkit.rb):
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
Rails.application.config.wallet_passkit.apple_pass_certificate_p12_path = Rails.root.join('config', 'certs', 'pass_cert.p12').to_s
|
48
|
+
Rails.application.config.wallet_passkit.apple_pass_certificate_password = ENV['APPLE_PASS_P12_PASSWORD']
|
49
|
+
Rails.application.config.wallet_passkit.apple_wwdr_certificate_path = Rails.root.join('config', 'certs', 'AppleWWDRCAG3.pem').to_s
|
50
|
+
Rails.application.config.wallet_passkit.apple_team_identifier = ENV['APPLE_TEAM_ID']
|
51
|
+
Rails.application.config.wallet_passkit.apple_organization_name = 'My Org'
|
52
|
+
|
53
|
+
Rails.application.config.wallet_passkit.google_service_account_credentials = Rails.root.join('config', 'google', 'service_account.json').to_s
|
54
|
+
```
|
55
|
+
|
56
|
+
## Apple Wallet: Generate a .pkpass
|
57
|
+
|
58
|
+
At minimum you need:
|
59
|
+
- A pass type ID certificate (.p12) and password
|
60
|
+
- Apple WWDR certificate (PEM)
|
61
|
+
- A `pass.json` payload
|
62
|
+
- Required images (icon.png; icon@2x.png recommended)
|
63
|
+
|
64
|
+
Example in Rails controller action:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
payload = WalletPasskit::Apple::Service.build_pass_payload(
|
68
|
+
description: 'Loyalty Card',
|
69
|
+
pass_type_identifier: 'pass.com.example.loyalty',
|
70
|
+
serial_number: 'ABC123',
|
71
|
+
logo_text: 'My Store',
|
72
|
+
primary_fields: [ { key: 'points', label: 'Points', value: '100' } ],
|
73
|
+
background_color: 'rgb(255,0,0)',
|
74
|
+
foreground_color: 'rgb(255,255,255)'
|
75
|
+
)
|
76
|
+
|
77
|
+
assets = {
|
78
|
+
'icon.png' => File.binread(Rails.root.join('app/assets/images/pass/icon.png')),
|
79
|
+
'icon@2x.png' => File.binread(Rails.root.join('app/assets/images/pass/icon@2x.png')),
|
80
|
+
# add logo.png, strip.png, background.png as needed
|
81
|
+
}
|
82
|
+
|
83
|
+
pkpass_binary = WalletPasskit::Apple::Service.generate_pkpass(pass_payload: payload, assets: assets)
|
84
|
+
|
85
|
+
send_data pkpass_binary, filename: 'loyalty.pkpass', type: 'application/vnd.apple.pkpass'
|
86
|
+
```
|
87
|
+
|
88
|
+
If you prefer to fully control pass.json, just pass your own hash to `generate_pkpass`.
|
89
|
+
|
90
|
+
## Google Wallet
|
91
|
+
|
92
|
+
For step-by-step setup (Issuer, API enablement, Service Account, LoyaltyClass) and usage examples (Save link and updates), see the dedicated guide:
|
93
|
+
- https://github.com/gioggi/wallet_passkit/blob/main/lib/wallet_passkit/google/README.md
|
94
|
+
|
95
|
+
This gem generates a Save to Google Wallet link (JWT) and provides a minimal updater for loyalty points.
|
96
|
+
|
97
|
+
## Development
|
98
|
+
|
99
|
+
- Run tests: `bundle exec rspec`
|
100
|
+
- Lint: pending
|
101
|
+
|
102
|
+
## Notes & Limitations
|
103
|
+
|
104
|
+
- Apple requires asset files to be included and a correctly signed manifest. This gem signs with PKCS#7 (DER) using your pass certificate and Apple WWDR intermediate.
|
105
|
+
- You must handle obtaining and managing certificates/keys securely.
|
106
|
+
- For Google Wallet, this gem builds a Save link JWT and provides a minimal updater; for full class/object management, extend accordingly.
|
107
|
+
|
108
|
+
## License
|
109
|
+
|
110
|
+
MIT
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "json"
|
5
|
+
require "digest/sha1"
|
6
|
+
require "zip"
|
7
|
+
|
8
|
+
module WalletPasskit
|
9
|
+
module Apple
|
10
|
+
class Service
|
11
|
+
def self.build_pass_payload(description:, pass_type_identifier:, serial_number:, logo_text:, primary_fields: [], secondary_fields: [], auxiliary_fields: [], back_fields: [], team_identifier: nil, organization_name: nil, pass_type: :storeCard, background_color: nil, label_color: nil, foreground_color: nil)
|
12
|
+
team_identifier ||= WalletPasskit.config.apple_team_identifier
|
13
|
+
organization_name ||= WalletPasskit.config.apple_organization_name
|
14
|
+
|
15
|
+
base = {
|
16
|
+
formatVersion: 1,
|
17
|
+
description: description,
|
18
|
+
organizationName: organization_name,
|
19
|
+
teamIdentifier: team_identifier,
|
20
|
+
passTypeIdentifier: pass_type_identifier,
|
21
|
+
serialNumber: serial_number,
|
22
|
+
logoText: logo_text
|
23
|
+
}
|
24
|
+
|
25
|
+
section_key = pass_type.to_sym == :storeCard ? :storeCard : :generic
|
26
|
+
section_payload = {}
|
27
|
+
section_payload[:primaryFields] = primary_fields if primary_fields && !primary_fields.empty?
|
28
|
+
section_payload[:secondaryFields] = secondary_fields if secondary_fields && !secondary_fields.empty?
|
29
|
+
section_payload[:auxiliaryFields] = auxiliary_fields if auxiliary_fields && !auxiliary_fields.empty?
|
30
|
+
section_payload[:backFields] = back_fields if back_fields && !back_fields.empty?
|
31
|
+
|
32
|
+
base[section_key] = section_payload unless section_payload.empty?
|
33
|
+
|
34
|
+
base[:backgroundColor] = background_color if background_color
|
35
|
+
base[:labelColor] = label_color if label_color
|
36
|
+
base[:foregroundColor] = foreground_color if foreground_color
|
37
|
+
|
38
|
+
base
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.generate_pkpass(pass_payload:, assets: {})
|
42
|
+
p12_path = WalletPasskit.config.apple_pass_certificate_p12_path
|
43
|
+
p12_password = WalletPasskit.config.apple_pass_certificate_password
|
44
|
+
wwdr_path = WalletPasskit.config.apple_wwdr_certificate_path
|
45
|
+
|
46
|
+
raise WalletPasskit::Error, "Missing Apple certificate configuration" unless p12_path && p12_password && wwdr_path
|
47
|
+
|
48
|
+
files = { "pass.json" => JSON.pretty_generate(pass_payload) }
|
49
|
+
files.merge!(assets)
|
50
|
+
|
51
|
+
manifest = {}
|
52
|
+
files.each do |name, content|
|
53
|
+
manifest[name] = Digest::SHA1.hexdigest(content)
|
54
|
+
end
|
55
|
+
manifest_json = JSON.generate(manifest)
|
56
|
+
|
57
|
+
signature_der = WalletPasskit::Apple::Signer.sign(
|
58
|
+
data: manifest_json,
|
59
|
+
p12_path: p12_path,
|
60
|
+
p12_password: p12_password,
|
61
|
+
wwdr_path: wwdr_path
|
62
|
+
)
|
63
|
+
|
64
|
+
io = StringIO.new
|
65
|
+
Zip::OutputStream.write_buffer(io) do |zip|
|
66
|
+
files.each do |name, content|
|
67
|
+
zip.put_next_entry(name)
|
68
|
+
zip.write(content)
|
69
|
+
end
|
70
|
+
|
71
|
+
zip.put_next_entry("manifest.json")
|
72
|
+
zip.write(manifest_json)
|
73
|
+
|
74
|
+
zip.put_next_entry("signature")
|
75
|
+
zip.write(signature_der)
|
76
|
+
end
|
77
|
+
io.rewind
|
78
|
+
io.read
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
|
5
|
+
module WalletPasskit
|
6
|
+
module Apple
|
7
|
+
class Signer
|
8
|
+
def self.sign(data:, p12_path:, p12_password:, wwdr_path:)
|
9
|
+
p12 = OpenSSL::PKCS12.new(File.binread(p12_path), p12_password)
|
10
|
+
key = p12.key
|
11
|
+
cert = p12.certificate
|
12
|
+
wwdr = OpenSSL::X509::Certificate.new(File.read(wwdr_path))
|
13
|
+
|
14
|
+
store = OpenSSL::X509::Store.new
|
15
|
+
store.add_cert(wwdr) rescue nil
|
16
|
+
|
17
|
+
flags = OpenSSL::PKCS7::BINARY | OpenSSL::PKCS7::DETACHED
|
18
|
+
pkcs7 = OpenSSL::PKCS7.sign(cert, key, data, [wwdr], flags)
|
19
|
+
pkcs7.to_der
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WalletPasskit
|
4
|
+
# Global configuration container
|
5
|
+
class Configuration
|
6
|
+
# Apple Wallet certs configuration
|
7
|
+
attr_accessor :apple_pass_certificate_p12_path, :apple_pass_certificate_password,
|
8
|
+
:apple_wwdr_certificate_path, :apple_team_identifier, :apple_organization_name
|
9
|
+
|
10
|
+
# Google Wallet configuration (service account credentials JSON path or hash)
|
11
|
+
attr_accessor :google_service_account_credentials, :google_issuer_id, :google_class_prefix
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@apple_pass_certificate_p12_path = nil
|
15
|
+
@apple_pass_certificate_password = nil
|
16
|
+
@apple_wwdr_certificate_path = nil
|
17
|
+
@apple_team_identifier = nil
|
18
|
+
@apple_organization_name = nil
|
19
|
+
|
20
|
+
@google_service_account_credentials = nil # path or parsed hash
|
21
|
+
@google_issuer_id = nil
|
22
|
+
@google_class_prefix = nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# WalletPasskit – Google Wallet Guide
|
2
|
+
|
3
|
+
Helpers to create Save to Google Wallet links and update Loyalty cards using Ruby.
|
4
|
+
|
5
|
+
### What you can do
|
6
|
+
- Generate a "Save to Google Wallet" link (JWT)
|
7
|
+
- Customize brand colors, logo, hero image, and store locations
|
8
|
+
- Assign a QR/barcode and dynamic loyalty points
|
9
|
+
- Update points later via REST (PATCH)
|
10
|
+
|
11
|
+
---
|
12
|
+
|
13
|
+
## Prerequisites
|
14
|
+
- A Google Wallet Issuer account for your business
|
15
|
+
- A Google Cloud project with the Google Wallet API enabled
|
16
|
+
- A Service Account with a JSON key file
|
17
|
+
|
18
|
+
---
|
19
|
+
|
20
|
+
## Step-by-step setup
|
21
|
+
|
22
|
+
### 1) Request/Configure your Issuer
|
23
|
+
- Open the Google Pay & Wallet Console: `https://pay.google.com/business/console`
|
24
|
+
- Request an Issuer account if you do not have one yet and complete verification (business info, branding, domain if applicable)
|
25
|
+
- Note your Issuer ID (a long numeric id, e.g. `3388000000000000000`)
|
26
|
+
|
27
|
+
### 2) Enable the API in Google Cloud
|
28
|
+
- Go to `https://console.cloud.google.com` and select or create a project
|
29
|
+
- Enable "Google Wallet API" from the API Library
|
30
|
+
|
31
|
+
### 3) Create a Service Account + key
|
32
|
+
- In Google Cloud Console: IAM & Admin → Service Accounts → Create
|
33
|
+
- Grant a role that can access Wallet Objects (Editor is sufficient for development)
|
34
|
+
- Create a JSON key and download it
|
35
|
+
- Store it in your app, e.g. `config/google/service_account.json`
|
36
|
+
|
37
|
+
### 4) Grant the Service Account access to the Issuer
|
38
|
+
- In the Google Pay & Wallet Console, add the service account email as a member/collaborator for your Issuer so it can create/update objects
|
39
|
+
|
40
|
+
### 5) Create a LoyaltyClass
|
41
|
+
Your passes (objects) must reference an existing LoyaltyClass.
|
42
|
+
|
43
|
+
You can create it via the Console (if available) or via REST API. The class id format is:
|
44
|
+
|
45
|
+
- `loyaltyClass.id = "<ISSUER_ID>.<CLASS_ID>"` (e.g., `3388000000000000000.keristo_loyalty`)
|
46
|
+
|
47
|
+
Minimal LoyaltyClass via REST:
|
48
|
+
|
49
|
+
```bash
|
50
|
+
curl -X POST \
|
51
|
+
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
|
52
|
+
-H "Content-Type: application/json" \
|
53
|
+
-d '{
|
54
|
+
"id": "3388000000000000000.keristo_loyalty",
|
55
|
+
"issuerName": "My Brand",
|
56
|
+
"programName": "Keristo Loyalty",
|
57
|
+
"reviewStatus": "underReview"
|
58
|
+
}' \
|
59
|
+
https://walletobjects.googleapis.com/walletobjects/v1/loyaltyClass
|
60
|
+
```
|
61
|
+
|
62
|
+
You can add branding fields later (logo, colors, etc.) directly on the class or customize per-object.
|
63
|
+
|
64
|
+
---
|
65
|
+
|
66
|
+
## Configuration in your app
|
67
|
+
Add the service account path to your configuration. For Rails:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
Rails.application.config.wallet_passkit.google_service_account_credentials = \
|
71
|
+
Rails.root.join("config", "google", "service_account.json").to_s
|
72
|
+
```
|
73
|
+
|
74
|
+
---
|
75
|
+
|
76
|
+
## Generate a Save to Google Wallet link
|
77
|
+
Build your company and customer hashes, then create the URL.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
company = {
|
81
|
+
issuer_id: ENV["GOOGLE_ISSUER_ID"], # e.g. "3388000000000000000"
|
82
|
+
class_id: "keristo_loyalty", # your LoyaltyClass suffix
|
83
|
+
background_color: "#112233", # optional
|
84
|
+
font_color: "#FFFFFF", # optional
|
85
|
+
logo_uri: "https://example.com/logo.png", # optional
|
86
|
+
hero_image_uri: "https://example.com/hero.png", # optional
|
87
|
+
locations: [ { latitude: 45.4642, longitude: 9.1900 } ] # optional
|
88
|
+
}
|
89
|
+
|
90
|
+
customer = {
|
91
|
+
id: "customer-123",
|
92
|
+
first_name: "Mario",
|
93
|
+
last_name: "Rossi",
|
94
|
+
points: 120,
|
95
|
+
qr_value: "UUID-1234" # or use :barcode_value and optional :barcode_type
|
96
|
+
}
|
97
|
+
|
98
|
+
url = WalletPasskit::Google::Pass.new(
|
99
|
+
company: company,
|
100
|
+
customer: customer,
|
101
|
+
service_account_path: Rails.application.config.wallet_passkit.google_service_account_credentials
|
102
|
+
).save_url
|
103
|
+
# Redirect the user to `url`, or render it as a link/button
|
104
|
+
```
|
105
|
+
|
106
|
+
What the gem generates
|
107
|
+
- A JWT with `payload.loyaltyObjects[0]` that includes your colors, images, locations, and the customer’s points/barcode
|
108
|
+
- The Save URL: `https://pay.google.com/gp/v/save/<JWT>`
|
109
|
+
|
110
|
+
---
|
111
|
+
|
112
|
+
## Update points later (PATCH)
|
113
|
+
Use the object id format: `<ISSUER_ID>.<CUSTOMER_ID>`.
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
object_id = "#{company[:issuer_id]}.#{customer[:id]}"
|
117
|
+
|
118
|
+
WalletPasskit::Google::PassUpdater.new(
|
119
|
+
object_id: object_id,
|
120
|
+
service_account_path: Rails.application.config.wallet_passkit.google_service_account_credentials
|
121
|
+
).update_points(new_point_value: 480)
|
122
|
+
```
|
123
|
+
|
124
|
+
---
|
125
|
+
|
126
|
+
## Troubleshooting
|
127
|
+
- 401/403 errors: ensure the Service Account is added to your Issuer in the Wallet Console and the API is enabled
|
128
|
+
- Not found: verify the LoyaltyClass exists and your object references `classId = "<ISSUER_ID>.<CLASS_ID>"`
|
129
|
+
- Branding not visible: check whether you set colors/images on the class vs object; some UI elements derive from the class
|
130
|
+
|
131
|
+
---
|
132
|
+
|
133
|
+
## Useful links
|
134
|
+
- Developer site: `https://developers.google.com/wallet`
|
135
|
+
- Loyalty cards API reference: `https://developers.google.com/wallet/retail/loyalty-cards`
|
136
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "googleauth"
|
4
|
+
|
5
|
+
module WalletPasskit
|
6
|
+
module Google
|
7
|
+
module Auth
|
8
|
+
def self.access_token(service_account_path:)
|
9
|
+
credentials = ::Google::Auth::ServiceAccountCredentials.make_creds(
|
10
|
+
json_key_io: File.open(service_account_path),
|
11
|
+
scope: ["https://www.googleapis.com/auth/wallet_object.issuer"]
|
12
|
+
)
|
13
|
+
credentials.fetch_access_token!
|
14
|
+
credentials.access_token
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "googleauth"
|
4
|
+
require "jwt"
|
5
|
+
require "net/http"
|
6
|
+
require "uri"
|
7
|
+
require "json"
|
8
|
+
|
9
|
+
module WalletPasskit
|
10
|
+
module Google
|
11
|
+
class Pass
|
12
|
+
def initialize(company:, customer:, service_account_path:)
|
13
|
+
@company = company.transform_keys(&:to_sym)
|
14
|
+
@customer = customer.transform_keys(&:to_sym)
|
15
|
+
@service_account_path = service_account_path
|
16
|
+
end
|
17
|
+
|
18
|
+
def save_url
|
19
|
+
# For save URL, we need a simplified object structure
|
20
|
+
# The full object structure is only for API calls
|
21
|
+
save_object = {
|
22
|
+
id: "#{issuer_id}.#{@customer[:id]}",
|
23
|
+
classId: "#{issuer_id}.#{@company[:class_id]}",
|
24
|
+
state: "active",
|
25
|
+
accountId: @customer[:id],
|
26
|
+
accountName: "#{@customer[:first_name]} #{@customer[:last_name]}",
|
27
|
+
loyaltyPoints: {
|
28
|
+
label: "Punti",
|
29
|
+
balance: {
|
30
|
+
int: @customer[:points] || 0
|
31
|
+
}
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
# Add optional fields for save URL
|
36
|
+
if @company[:background_color]
|
37
|
+
save_object[:hexBackgroundColor] = @company[:background_color]
|
38
|
+
end
|
39
|
+
|
40
|
+
if @company[:font_color]
|
41
|
+
save_object[:hexFontColor] = @company[:font_color]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Add barcode for save URL
|
45
|
+
if @customer[:qr_value]
|
46
|
+
save_object[:barcode] = {
|
47
|
+
type: "QR_CODE",
|
48
|
+
value: @customer[:qr_value]
|
49
|
+
}
|
50
|
+
elsif @customer[:barcode_value]
|
51
|
+
save_object[:barcode] = {
|
52
|
+
type: (@customer[:barcode_type] || "QR_CODE"),
|
53
|
+
value: @customer[:barcode_value]
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
payload = {
|
58
|
+
iss: service_account_email,
|
59
|
+
aud: "google",
|
60
|
+
typ: "savetowallet",
|
61
|
+
payload: {
|
62
|
+
loyaltyObjects: [save_object]
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
jwt = JWT.encode(payload, private_key, "RS256", kid: key_id)
|
67
|
+
"https://pay.google.com/gp/v/save/#{jwt}"
|
68
|
+
end
|
69
|
+
|
70
|
+
# Creates a loyalty object in Google Wallet using the API
|
71
|
+
def create_loyalty_object
|
72
|
+
loyalty_object_data = build_loyalty_object
|
73
|
+
|
74
|
+
uri = URI("https://walletobjects.googleapis.com/walletobjects/v1/loyaltyObject")
|
75
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
76
|
+
req["Authorization"] = "Bearer #{access_token}"
|
77
|
+
req.body = loyalty_object_data.to_json
|
78
|
+
|
79
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
80
|
+
http.request(req)
|
81
|
+
end
|
82
|
+
|
83
|
+
if res.is_a?(Net::HTTPSuccess)
|
84
|
+
created_object = JSON.parse(res.body)
|
85
|
+
puts "✅ Loyalty object created successfully!"
|
86
|
+
puts " ID: #{created_object['id']}"
|
87
|
+
puts " State: #{created_object['state']}"
|
88
|
+
created_object
|
89
|
+
else
|
90
|
+
error_message = "Failed to create loyalty object: #{res.code} #{res.message}"
|
91
|
+
if res.body
|
92
|
+
error_data = JSON.parse(res.body) rescue nil
|
93
|
+
if error_data && error_data['error']
|
94
|
+
error_message += " - #{error_data['error']['message']}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
raise WalletPasskit::Error, error_message
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Creates loyalty object and returns the save URL
|
102
|
+
def create_and_save_url
|
103
|
+
create_loyalty_object
|
104
|
+
save_url
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def access_token
|
110
|
+
WalletPasskit::Google::Auth.access_token(service_account_path: @service_account_path)
|
111
|
+
end
|
112
|
+
|
113
|
+
def service_account_email
|
114
|
+
JSON.parse(File.read(@service_account_path))["client_email"]
|
115
|
+
end
|
116
|
+
|
117
|
+
def issuer_id
|
118
|
+
@company[:issuer_id]
|
119
|
+
end
|
120
|
+
|
121
|
+
def key_id
|
122
|
+
JSON.parse(File.read(@service_account_path))["private_key_id"]
|
123
|
+
end
|
124
|
+
|
125
|
+
def private_key
|
126
|
+
OpenSSL::PKey::RSA.new(
|
127
|
+
JSON.parse(File.read(@service_account_path))["private_key"]
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
def build_loyalty_object
|
132
|
+
object = {
|
133
|
+
id: "#{issuer_id}.#{@customer[:id]}",
|
134
|
+
classId: "#{issuer_id}.#{@company[:class_id]}",
|
135
|
+
state: "active",
|
136
|
+
accountId: @customer[:id],
|
137
|
+
accountName: "#{@customer[:first_name]} #{@customer[:last_name]}",
|
138
|
+
loyaltyPoints: {
|
139
|
+
label: "Punti",
|
140
|
+
balance: {
|
141
|
+
int: @customer[:points] || 0
|
142
|
+
}
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
if @company[:background_color]
|
147
|
+
object[:hexBackgroundColor] = @company[:background_color]
|
148
|
+
end
|
149
|
+
|
150
|
+
if @company[:font_color]
|
151
|
+
object[:hexFontColor] = @company[:font_color]
|
152
|
+
end
|
153
|
+
|
154
|
+
image_modules = []
|
155
|
+
if @company[:logo_uri]
|
156
|
+
image_modules << {
|
157
|
+
mainImage: {
|
158
|
+
sourceUri: { uri: @company[:logo_uri] }
|
159
|
+
}
|
160
|
+
}
|
161
|
+
end
|
162
|
+
if @company[:hero_image_uri]
|
163
|
+
image_modules << {
|
164
|
+
mainImage: {
|
165
|
+
sourceUri: { uri: @company[:hero_image_uri] }
|
166
|
+
}
|
167
|
+
}
|
168
|
+
end
|
169
|
+
object[:imageModulesData] = image_modules unless image_modules.empty?
|
170
|
+
|
171
|
+
# Note: merchantLocations requires special authorization from Google
|
172
|
+
# For now, we'll skip locations to avoid API errors
|
173
|
+
# if @company[:locations].is_a?(Array)
|
174
|
+
# object[:merchantLocations] = @company[:locations].map do |loc|
|
175
|
+
# {
|
176
|
+
# latitude: loc[:latitude] || loc["latitude"],
|
177
|
+
# longitude: loc[:longitude] || loc["longitude"]
|
178
|
+
# }
|
179
|
+
# end
|
180
|
+
# end
|
181
|
+
|
182
|
+
if @customer[:qr_value]
|
183
|
+
object[:barcode] = {
|
184
|
+
type: "QR_CODE",
|
185
|
+
value: @customer[:qr_value]
|
186
|
+
}
|
187
|
+
elsif @customer[:barcode_value]
|
188
|
+
object[:barcode] = {
|
189
|
+
type: (@customer[:barcode_type] || "QR_CODE"),
|
190
|
+
value: @customer[:barcode_value]
|
191
|
+
}
|
192
|
+
end
|
193
|
+
|
194
|
+
object
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "uri"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
module WalletPasskit
|
8
|
+
module Google
|
9
|
+
class PassUpdater
|
10
|
+
def initialize(object_id:, service_account_path:)
|
11
|
+
@object_id = object_id
|
12
|
+
@service_account_path = service_account_path
|
13
|
+
end
|
14
|
+
|
15
|
+
def update_points(new_point_value:)
|
16
|
+
patch({ loyaltyPoints: { balance: { int: new_point_value }}})
|
17
|
+
end
|
18
|
+
|
19
|
+
# Recupera un oggetto loyalty specifico dal Google Wallet
|
20
|
+
def retrieve_object
|
21
|
+
uri = URI("https://walletobjects.googleapis.com/walletobjects/v1/loyaltyObject/#{@object_id}")
|
22
|
+
req = Net::HTTP::Get.new(uri)
|
23
|
+
req["Authorization"] = "Bearer #{access_token}"
|
24
|
+
|
25
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
26
|
+
http.request(req)
|
27
|
+
end
|
28
|
+
|
29
|
+
raise WalletPasskit::Error, "Errore API: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
|
30
|
+
JSON.parse(res.body)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Metodo pubblico per recuperare l'oggetto
|
34
|
+
def get_object
|
35
|
+
retrieve_object
|
36
|
+
end
|
37
|
+
|
38
|
+
# Metodi per ottenere informazioni specifiche dell'oggetto
|
39
|
+
def get_points
|
40
|
+
object = retrieve_object
|
41
|
+
object.dig('loyaltyPoints', 'balance', 'int') || 0
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_state
|
45
|
+
object = retrieve_object
|
46
|
+
object['state']
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_account_name
|
50
|
+
object = retrieve_object
|
51
|
+
object['accountName']
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_account_id
|
55
|
+
object = retrieve_object
|
56
|
+
object['accountId']
|
57
|
+
end
|
58
|
+
|
59
|
+
def get_creation_time
|
60
|
+
object = retrieve_object
|
61
|
+
object['createTime']
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_update_time
|
65
|
+
object = retrieve_object
|
66
|
+
object['updateTime']
|
67
|
+
end
|
68
|
+
|
69
|
+
# Metodo di classe per recuperare un oggetto senza creare un'istanza
|
70
|
+
def self.retrieve_object(object_id:, service_account_path:)
|
71
|
+
new(object_id: object_id, service_account_path: service_account_path).get_object
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def patch(data)
|
77
|
+
uri = URI("https://walletobjects.googleapis.com/walletobjects/v1/loyaltyObject/#{@object_id}")
|
78
|
+
req = Net::HTTP::Patch.new(uri, "Content-Type" => "application/json")
|
79
|
+
req["Authorization"] = "Bearer #{access_token}"
|
80
|
+
req.body = data.to_json
|
81
|
+
|
82
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
83
|
+
http.request(req)
|
84
|
+
end
|
85
|
+
|
86
|
+
raise WalletPasskit::Error, "Errore API: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
|
87
|
+
JSON.parse(res.body)
|
88
|
+
end
|
89
|
+
|
90
|
+
def access_token
|
91
|
+
WalletPasskit::Google::Auth.access_token(service_account_path: @service_account_path)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rails'
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
module WalletPasskit
|
9
|
+
if defined?(Rails)
|
10
|
+
class Railtie < ::Rails::Railtie
|
11
|
+
# Allow configuration via Rails.application.config.wallet_passkit
|
12
|
+
config.wallet_passkit = ActiveSupport::OrderedOptions.new
|
13
|
+
|
14
|
+
initializer 'wallet_passkit.configure' do |app|
|
15
|
+
cfg = app.config.wallet_passkit
|
16
|
+
WalletPasskit.configure do |c|
|
17
|
+
c.apple_pass_certificate_p12_path = cfg.apple_pass_certificate_p12_path if cfg.key?(:apple_pass_certificate_p12_path)
|
18
|
+
c.apple_pass_certificate_password = cfg.apple_pass_certificate_password if cfg.key?(:apple_pass_certificate_password)
|
19
|
+
c.apple_wwdr_certificate_path = cfg.apple_wwdr_certificate_path if cfg.key?(:apple_wwdr_certificate_path)
|
20
|
+
c.apple_team_identifier = cfg.apple_team_identifier if cfg.key?(:apple_team_identifier)
|
21
|
+
c.apple_organization_name = cfg.apple_organization_name if cfg.key?(:apple_organization_name)
|
22
|
+
|
23
|
+
c.google_service_account_credentials = cfg.google_service_account_credentials if cfg.key?(:google_service_account_credentials)
|
24
|
+
c.google_issuer_id = cfg.google_issuer_id if cfg.key?(:google_issuer_id)
|
25
|
+
c.google_class_prefix = cfg.google_class_prefix if cfg.key?(:google_class_prefix)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "json"
|
5
|
+
require "digest/sha1"
|
6
|
+
require "zip"
|
7
|
+
|
8
|
+
require_relative "wallet_passkit/version"
|
9
|
+
require_relative "wallet_passkit/configuration"
|
10
|
+
require_relative "wallet_passkit/google"
|
11
|
+
require_relative "wallet_passkit/apple"
|
12
|
+
require_relative "wallet_passkit/railtie"
|
13
|
+
require_relative "wallet_passkit/error"
|
14
|
+
|
15
|
+
module WalletPasskit
|
16
|
+
|
17
|
+
class Error < StandardError; end
|
18
|
+
|
19
|
+
def self.configure
|
20
|
+
yield(config)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.config
|
24
|
+
@config ||= Configuration.new
|
25
|
+
end
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wallet_passkit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gioggi
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rubyzip
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '2.3'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '2.3'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: jwt
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.7'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.7'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: googleauth
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.16.0
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.16.0
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: rake
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '13.0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '13.0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rspec
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3.12'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '3.12'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: webmock
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '3.0'
|
96
|
+
description: A modern, lightweight Ruby gem for generating Apple Wallet passes (.pkpass)
|
97
|
+
with OpenSSL signing, easy Rails integration services, and a minimal Google Wallet
|
98
|
+
Save link generator.
|
99
|
+
email:
|
100
|
+
- info@giovanniesposito.it
|
101
|
+
executables: []
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- README.md
|
106
|
+
- lib/wallet_passkit.rb
|
107
|
+
- lib/wallet_passkit/apple.rb
|
108
|
+
- lib/wallet_passkit/apple/service.rb
|
109
|
+
- lib/wallet_passkit/apple/signer.rb
|
110
|
+
- lib/wallet_passkit/configuration.rb
|
111
|
+
- lib/wallet_passkit/error.rb
|
112
|
+
- lib/wallet_passkit/google.rb
|
113
|
+
- lib/wallet_passkit/google/README.md
|
114
|
+
- lib/wallet_passkit/google/auth.rb
|
115
|
+
- lib/wallet_passkit/google/pass.rb
|
116
|
+
- lib/wallet_passkit/google/pass_updater.rb
|
117
|
+
- lib/wallet_passkit/railtie.rb
|
118
|
+
- lib/wallet_passkit/version.rb
|
119
|
+
homepage: https://example.com/wallet_passkit
|
120
|
+
licenses:
|
121
|
+
- MIT
|
122
|
+
metadata: {}
|
123
|
+
rdoc_options: []
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '2.7'
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubygems_version: 3.6.9
|
138
|
+
specification_version: 4
|
139
|
+
summary: Generate Apple Wallet .pkpass and integrate with Rails; scaffold for Google
|
140
|
+
Wallet
|
141
|
+
test_files: []
|