omniauth-clover-oauth2 1.0.0 → 1.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 718583b3f5599a91cb7c93b32f01af9ac8a658e3e351b4c601b9e5319a1345a3
4
- data.tar.gz: 152836810576c0efa3c17d4da5c56bfe6cda667f9a25c40e80fddd2ef1235081
3
+ metadata.gz: fb709fb251b03d2c45fce7d93faeaaea36f310b2f6fa3250122714834df7a7c1
4
+ data.tar.gz: eda34b1fe80cfb8b5ef604e161c82a3490b914042f8579cb19e9e98ad54d8d71
5
5
  SHA512:
6
- metadata.gz: 536d7e5a16bc73d381686f44ea96eeac9bf1225d6830a1a42aab054aeaf49552fcb48790cccb7473d4eed00f33b9521a71d9aab2906aed10a98a747c8b29d78e
7
- data.tar.gz: a6f5190005c3f2ec1e1cdec91991728bd2b3ba11fda0f67e121bf05016ff10703d00e0e1e14aef83fd2f704c80e09ed3abef7dfb0d4d9c8cb29d8f112a80313b
6
+ metadata.gz: 70e02544685ecc4e933c4ff32509563bd42790915b1993da626d684ebe0d906524f13328a0d5c0be0f20151322268f38ae6c12cdf379d306cb9ddf87f35a7666
7
+ data.tar.gz: 6de45329ff4570f9cfef702a162a8375b04e4f37f38ad5456736d74ef5721e03c3a53ca389b289f790c535296a9eeaecd5a34d54f58b457a7a723b861c110576
data/CHANGELOG.md CHANGED
@@ -5,7 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [1.0.0] - 2024-01-30
8
+ ## [1.1.1] - 2026-01-31
9
+
10
+ ### Fixed
11
+
12
+ - TokenClient now uses correct Clover OAuth v2 refresh endpoint (`/oauth/v2/refresh`)
13
+ - Fixed expiration parsing to handle Unix timestamps from Clover
14
+
15
+ ## [1.1.0] - 2026-01-30
16
+
17
+ ### Added
18
+
19
+ - TokenClient class for easy token refresh in Rails apps
20
+ - `refresh_token` method with result object pattern
21
+ - `token_expired?` helper with configurable buffer
22
+ - Comprehensive RSpec tests for TokenClient
23
+
24
+ ## [1.0.1] - 2026-01-30
25
+
26
+ ### Fixed
27
+
28
+ - Minor documentation and metadata updates
29
+
30
+ ## [1.0.0] - 2026-01-30
9
31
 
10
32
  ### Added
11
33
 
data/README.md CHANGED
@@ -231,7 +231,100 @@ end
231
231
 
232
232
  ## Token Refresh
233
233
 
234
- Clover access tokens expire (typically after 1 year, but check your app settings). To refresh tokens, you'll need to implement token refresh logic using the Clover API directly.
234
+ Clover access tokens expire (typically after 1 year, but check your app settings). This gem includes a `TokenClient` class to easily refresh tokens in your Rails app.
235
+
236
+ ### Basic Usage
237
+
238
+ ```ruby
239
+ # Create a client instance
240
+ client = OmniAuth::CloverOauth2::TokenClient.new(
241
+ client_id: ENV['CLOVER_CLIENT_ID'],
242
+ client_secret: ENV['CLOVER_CLIENT_SECRET'],
243
+ sandbox: Rails.env.development?
244
+ )
245
+
246
+ # Refresh an expired token
247
+ result = client.refresh_token(user.clover_refresh_token)
248
+
249
+ if result.success?
250
+ user.update!(
251
+ clover_access_token: result.access_token,
252
+ clover_refresh_token: result.refresh_token,
253
+ clover_token_expires_at: Time.at(result.expires_at)
254
+ )
255
+ else
256
+ Rails.logger.error "Token refresh failed: #{result.error}"
257
+ end
258
+ ```
259
+
260
+ ### Check Token Expiration
261
+
262
+ ```ruby
263
+ # Check if token is expired (with 5-minute buffer by default)
264
+ client.token_expired?(user.clover_token_expires_at)
265
+
266
+ # Custom buffer (e.g., refresh 1 hour before expiry)
267
+ client.token_expired?(user.clover_token_expires_at, buffer_seconds: 3600)
268
+ ```
269
+
270
+ ### Rails Service Example
271
+
272
+ ```ruby
273
+ # app/services/clover_api_service.rb
274
+ class CloverApiService
275
+ def initialize(user)
276
+ @user = user
277
+ @client = OmniAuth::CloverOauth2::TokenClient.new(
278
+ client_id: ENV['CLOVER_CLIENT_ID'],
279
+ client_secret: ENV['CLOVER_CLIENT_SECRET'],
280
+ sandbox: !Rails.env.production?
281
+ )
282
+ end
283
+
284
+ def with_valid_token
285
+ refresh_if_expired!
286
+ yield @user.clover_access_token
287
+ end
288
+
289
+ private
290
+
291
+ def refresh_if_expired!
292
+ return unless @client.token_expired?(@user.clover_token_expires_at)
293
+
294
+ result = @client.refresh_token(@user.clover_refresh_token)
295
+ raise "Token refresh failed: #{result.error}" unless result.success?
296
+
297
+ @user.update!(
298
+ clover_access_token: result.access_token,
299
+ clover_refresh_token: result.refresh_token,
300
+ clover_token_expires_at: Time.at(result.expires_at)
301
+ )
302
+ end
303
+ end
304
+
305
+ # Usage
306
+ CloverApiService.new(current_user).with_valid_token do |token|
307
+ # Make API calls with valid token
308
+ response = Faraday.get("https://api.clover.com/v3/merchants/#{merchant_id}") do |req|
309
+ req.headers['Authorization'] = "Bearer #{token}"
310
+ end
311
+ end
312
+ ```
313
+
314
+ ### TokenResult Object
315
+
316
+ The `refresh_token` method returns a `TokenResult` object with:
317
+
318
+ | Method | Description |
319
+ |--------|-------------|
320
+ | `success?` | Returns `true` if refresh succeeded |
321
+ | `failure?` | Returns `true` if refresh failed |
322
+ | `access_token` | The new access token |
323
+ | `refresh_token` | The new refresh token |
324
+ | `expires_at` | Unix timestamp when token expires |
325
+ | `expires_in` | Seconds until token expires |
326
+ | `error` | Error message if failed |
327
+ | `raw_response` | Full response hash from Clover |
235
328
 
236
329
  ## Contributing
237
330
 
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'json'
5
+
6
+ module OmniAuth
7
+ module CloverOauth2
8
+ # Client for managing Clover OAuth2 tokens
9
+ #
10
+ # This class provides an easy way to refresh expired tokens in your Rails app.
11
+ #
12
+ # @example Basic usage
13
+ # client = OmniAuth::CloverOauth2::TokenClient.new(
14
+ # client_id: ENV['CLOVER_CLIENT_ID'],
15
+ # client_secret: ENV['CLOVER_CLIENT_SECRET'],
16
+ # sandbox: Rails.env.development?
17
+ # )
18
+ #
19
+ # # Refresh an expired token
20
+ # result = client.refresh_token(user.clover_refresh_token)
21
+ # if result.success?
22
+ # user.update!(
23
+ # clover_access_token: result.access_token,
24
+ # clover_refresh_token: result.refresh_token,
25
+ # clover_token_expires_at: result.expires_at
26
+ # )
27
+ # end
28
+ #
29
+ # @example With automatic token refresh in a service
30
+ # class CloverApiService
31
+ # def initialize(user)
32
+ # @user = user
33
+ # @client = OmniAuth::CloverOauth2::TokenClient.new(
34
+ # client_id: ENV['CLOVER_CLIENT_ID'],
35
+ # client_secret: ENV['CLOVER_CLIENT_SECRET']
36
+ # )
37
+ # end
38
+ #
39
+ # def with_valid_token
40
+ # refresh_if_expired!
41
+ # yield @user.clover_access_token
42
+ # end
43
+ #
44
+ # private
45
+ #
46
+ # def refresh_if_expired!
47
+ # return unless @user.clover_token_expired?
48
+ #
49
+ # result = @client.refresh_token(@user.clover_refresh_token)
50
+ # raise "Token refresh failed: #{result.error}" unless result.success?
51
+ #
52
+ # @user.update!(
53
+ # clover_access_token: result.access_token,
54
+ # clover_refresh_token: result.refresh_token,
55
+ # clover_token_expires_at: result.expires_at
56
+ # )
57
+ # end
58
+ # end
59
+ #
60
+ class TokenClient
61
+ # Result object for token operations
62
+ class TokenResult
63
+ attr_reader :access_token, :refresh_token, :expires_at, :expires_in, :error, :raw_response
64
+
65
+ def initialize(success:, access_token: nil, refresh_token: nil, expires_at: nil, expires_in: nil,
66
+ error: nil, raw_response: nil)
67
+ @success = success
68
+ @access_token = access_token
69
+ @refresh_token = refresh_token
70
+ @expires_at = expires_at
71
+ @expires_in = expires_in
72
+ @error = error
73
+ @raw_response = raw_response
74
+ end
75
+
76
+ def success?
77
+ @success
78
+ end
79
+
80
+ def failure?
81
+ !@success
82
+ end
83
+ end
84
+
85
+ # OAuth v2 refresh uses the API domain with /oauth/v2/refresh
86
+ SANDBOX_API_URL = 'https://apisandbox.dev.clover.com'
87
+ PRODUCTION_API_URL = 'https://api.clover.com'
88
+ EU_API_URL = 'https://api.eu.clover.com'
89
+ LA_API_URL = 'https://api.la.clover.com'
90
+
91
+ attr_reader :client_id, :client_secret, :sandbox
92
+
93
+ # Initialize a new TokenClient
94
+ #
95
+ # @param client_id [String] Your Clover App ID
96
+ # @param client_secret [String] Your Clover App Secret
97
+ # @param sandbox [Boolean] Use sandbox environment (default: true)
98
+ def initialize(client_id:, client_secret:, sandbox: true)
99
+ @client_id = client_id
100
+ @client_secret = client_secret
101
+ @sandbox = sandbox
102
+ end
103
+
104
+ # Refresh an access token using a refresh token
105
+ #
106
+ # @param refresh_token [String] The refresh token to use
107
+ # @return [TokenResult] Result object with new tokens or error
108
+ def refresh_token(refresh_token)
109
+ return TokenResult.new(success: false, error: 'Refresh token is required') if refresh_token.nil? || refresh_token.empty?
110
+
111
+ response = make_refresh_request(refresh_token)
112
+
113
+ if response.success?
114
+ parse_success_response(response)
115
+ else
116
+ parse_error_response(response)
117
+ end
118
+ rescue Faraday::Error => e
119
+ TokenResult.new(success: false, error: "Network error: #{e.message}")
120
+ rescue JSON::ParserError => e
121
+ TokenResult.new(success: false, error: "Invalid JSON response: #{e.message}")
122
+ rescue StandardError => e
123
+ TokenResult.new(success: false, error: "Unexpected error: #{e.message}")
124
+ end
125
+
126
+ # Check if a token is expired or about to expire
127
+ #
128
+ # @param expires_at [Time, Integer] Token expiration time
129
+ # @param buffer_seconds [Integer] Buffer before expiration (default: 300 = 5 minutes)
130
+ # @return [Boolean] True if token is expired or will expire within buffer
131
+ def token_expired?(expires_at, buffer_seconds: 300)
132
+ return true if expires_at.nil?
133
+
134
+ expires_at_time = expires_at.is_a?(Integer) ? Time.at(expires_at) : expires_at
135
+ Time.now >= (expires_at_time - buffer_seconds)
136
+ end
137
+
138
+ # Get the API base URL based on sandbox setting
139
+ #
140
+ # @return [String] The API base URL
141
+ def api_base_url
142
+ sandbox ? SANDBOX_API_URL : PRODUCTION_API_URL
143
+ end
144
+
145
+ private
146
+
147
+ def make_refresh_request(refresh_token)
148
+ # Clover OAuth v2 refresh endpoint uses JSON body
149
+ # Endpoint: /oauth/v2/refresh
150
+ # Required params: client_id, refresh_token
151
+ Faraday.post(refresh_url) do |req|
152
+ req.headers['Content-Type'] = 'application/json'
153
+ req.headers['Accept'] = 'application/json'
154
+ req.body = {
155
+ client_id: client_id,
156
+ refresh_token: refresh_token
157
+ }.to_json
158
+ end
159
+ end
160
+
161
+ def refresh_url
162
+ # OAuth v2 uses /oauth/v2/refresh endpoint
163
+ "#{api_base_url}/oauth/v2/refresh"
164
+ end
165
+
166
+ def parse_success_response(response)
167
+ data = JSON.parse(response.body)
168
+
169
+ # Clover OAuth v2 returns 'access_token_expiration' as Unix timestamp
170
+ # It may also return 'expires_in' as seconds in some contexts
171
+ raw_expiration = data['access_token_expiration'] || data['expires_in']
172
+
173
+ # Determine if it's a timestamp (large number) or seconds (small number)
174
+ # Unix timestamps for 2024+ are > 1700000000
175
+ if raw_expiration && raw_expiration.to_i > 1_600_000_000
176
+ # It's a Unix timestamp
177
+ expires_at = raw_expiration.to_i
178
+ expires_in = expires_at - Time.now.to_i
179
+ elsif raw_expiration
180
+ # It's seconds until expiry
181
+ expires_in = raw_expiration.to_i
182
+ expires_at = Time.now.to_i + expires_in
183
+ else
184
+ expires_in = nil
185
+ expires_at = nil
186
+ end
187
+
188
+ TokenResult.new(
189
+ success: true,
190
+ access_token: data['access_token'],
191
+ refresh_token: data['refresh_token'],
192
+ expires_in: expires_in,
193
+ expires_at: expires_at,
194
+ raw_response: data
195
+ )
196
+ end
197
+
198
+ def parse_error_response(response)
199
+ error_data = begin
200
+ JSON.parse(response.body)
201
+ rescue JSON::ParserError
202
+ { 'message' => response.body }
203
+ end
204
+
205
+ error_message = error_data['message'] || error_data['error'] || "HTTP #{response.status}"
206
+
207
+ TokenResult.new(
208
+ success: false,
209
+ error: error_message,
210
+ raw_response: error_data
211
+ )
212
+ end
213
+ end
214
+ end
215
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OmniAuth
4
4
  module CloverOauth2
5
- VERSION = '1.0.0'
5
+ VERSION = '1.1.1'
6
6
  end
7
7
  end
@@ -2,4 +2,5 @@
2
2
 
3
3
  require 'omniauth-oauth2'
4
4
  require 'omniauth/clover_oauth2/version'
5
+ require 'omniauth/clover_oauth2/token_client'
5
6
  require 'omniauth/strategies/clover_oauth2'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth-clover-oauth2
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - dan1d
@@ -181,6 +181,7 @@ files:
181
181
  - LICENSE.txt
182
182
  - README.md
183
183
  - lib/omniauth-clover-oauth2.rb
184
+ - lib/omniauth/clover_oauth2/token_client.rb
184
185
  - lib/omniauth/clover_oauth2/version.rb
185
186
  - lib/omniauth/strategies/clover_oauth2.rb
186
187
  homepage: https://github.com/dan1d/omniauth-clover-oauth2