rails-shopee-qris 0.2.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +20 -7
- data/README.md +13 -7
- data/lib/rails/shopee/qris/client.rb +33 -34
- data/lib/rails/shopee/qris/qris.rb +1 -1
- data/lib/rails/shopee/qris/response.rb +5 -3
- data/lib/rails/shopee/qris/setup.rb +58 -32
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c540bf182092132cfb2ffc55a0b47e1a5e83591e99ef57d1592717ac0b9aeeb7
|
|
4
|
+
data.tar.gz: bff1c88608517ed8258c727d3913693aea35b8b14006520545bf1552bacb02c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 11943a5f4d2f2398b6fa00a1973b479752cd1a3a49be0364529a1fedf7795fa392bcaa3b7f2f03a2d1fcf2e4bd89cd61ff46299d025c3d5bc747ef9e1c56f0f3
|
|
7
|
+
data.tar.gz: 65bbab7fc0cf7fd5ccf73800f3deb0fe4bf8766566e0e5e16920bcecb3caf339ad444316c1f7366a634a1496d526a508925985edf11ee6f0b756531a32750376
|
data/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
- Do not infer merchant ID from JWT staff `userid`.
|
|
6
|
+
- Reject malformed credentials, invalid payment envelopes, and invalid time ranges.
|
|
7
|
+
- Do not suppress unknown password-auth failures or captcha requirements.
|
|
8
|
+
- Preserve provider rejection codes without claiming an undocumented expiry reason.
|
|
9
|
+
- Expose optional OTP `seed` as opaque response data, not delivery evidence.
|
|
10
|
+
- Reject malformed transaction/store lists rather than returning misleading empty history.
|
|
11
|
+
- Preserve selected merchant scope when constructing the OTP data client.
|
|
12
|
+
- Reject fractional provider result codes and malformed OTP channel settings.
|
|
13
|
+
- Correct prior verification claims: no successful OTP login or payment settlement observed.
|
|
14
|
+
|
|
3
15
|
## 0.2.0
|
|
4
16
|
|
|
5
|
-
|
|
6
|
-
|
|
17
|
+
Observed store listing and an empty transaction feed with a merchant token. OTP
|
|
18
|
+
requests returned success without recipient-confirmed delivery. Transaction detail,
|
|
19
|
+
successful OTP login and payment settlement were not verified live.
|
|
7
20
|
|
|
8
21
|
### Added
|
|
9
22
|
- `Client` accepts either the raw `B:...` merchant token or the whole
|
|
10
|
-
`__shopee_partner_website_x_token_live` JWT cookie, extracting the inner token
|
|
11
|
-
|
|
23
|
+
`__shopee_partner_website_x_token_live` JWT cookie, extracting the inner token
|
|
24
|
+
and advisory expiry.
|
|
12
25
|
- `Client#refreshable?` tells callers whether a session can silently renew.
|
|
13
26
|
- Provider error codes are translated into actionable text
|
|
14
27
|
(`10002`, `200020`, `2010000`, `48401003`, `48401102`, `48401103`, `48500102`).
|
|
@@ -20,14 +33,14 @@ feed, transaction detail, device-risk, OTP request).
|
|
|
20
33
|
- **Payment envelope shape.** Request fields were nested under `data.metadata`; Shopee
|
|
21
34
|
expects them as siblings of `metadata`. Store listing and the transaction feed never
|
|
22
35
|
worked before this.
|
|
23
|
-
- **`10002`
|
|
24
|
-
|
|
36
|
+
- **`10002` compatibility.** Empty-password responses were allowed to continue to the
|
|
37
|
+
OTP step. Its meaning was not independently established.
|
|
25
38
|
- **Wrong refresh error.** A rejected manual token raised "session cannot be refreshed";
|
|
26
39
|
it now reports the real rejection and its code. Proactive refresh only runs when the
|
|
27
40
|
session actually has renewal credentials.
|
|
28
41
|
- **Duplicate-key crash.** `Client` symbolized sessions via `JSON.generate`, which raised
|
|
29
42
|
on hashes carrying both `"token"` and `:token`.
|
|
30
|
-
- **`48401003`** is
|
|
43
|
+
- **`48401003`** is reported as OTP rejection rather than a bare code; reason unspecified.
|
|
31
44
|
- QRIS generation rejects fractional and oversized amounts instead of truncating them,
|
|
32
45
|
and parsing rejects non-decimal lengths and duplicate tags.
|
|
33
46
|
|
data/README.md
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Unofficial Ruby/Rails ShopeePay Partner client, based on `QrisMerchantID` Shopee API logic and `rails-gopay-unofficial` gem structure. Ruby >= 3.1; no runtime dependencies beyond stdlib.
|
|
4
4
|
|
|
5
|
-
Use only with merchant authorization. Internal Shopee APIs can change.
|
|
5
|
+
Use only with merchant authorization. Internal Shopee APIs can change. Store listing and an empty transaction feed were observed with a merchant token. OTP endpoints accepted requests, but recipients reported no delivery; successful OTP login and money settlement remain unverified. Never log tokens, passwords, device reports, session cookies, or raw payment data.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Install published version from RubyGems:
|
|
10
10
|
|
|
11
11
|
```ruby
|
|
12
|
-
gem "rails-shopee-qris",
|
|
12
|
+
gem "rails-shopee-qris", "~> 0.2.0"
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
Version 0.2.1 contains the reviewed validation and merchant-scope fixes; it must be published before RubyGems consumers can install it. To exercise this checkout locally, use `gem "rails-shopee-qris", path: "../rails-shopee-qris"`.
|
|
16
|
+
|
|
15
17
|
```ruby
|
|
16
18
|
require "rails/shopee/qris"
|
|
17
19
|
|
|
@@ -24,7 +26,7 @@ end
|
|
|
24
26
|
|
|
25
27
|
## Direct merchant token
|
|
26
28
|
|
|
27
|
-
Pass either the inner `B:...` token
|
|
29
|
+
Pass either the inner `B:...` token or the whole `__shopee_partner_website_x_token_live` cookie value. Client extracts the inner token and advisory expiry. JWT `userid` is a staff account ID, **not** merchant ID; provide merchant scope explicitly when known. JWT decoding does not verify its signature; Shopee validates credentials on each request.
|
|
28
30
|
|
|
29
31
|
Get a fresh token: log in at `partner.shopee.co.id` → DevTools → Network → open the ShopeePay transaction history page → click `get-transaction-list` → Request Payload → `data.metadata.token`.
|
|
30
32
|
|
|
@@ -64,6 +66,10 @@ Channels: `1` SMS, `2` voice, `3` WhatsApp, `4` email, `5` Zalo. Omit `channel:`
|
|
|
64
66
|
|
|
65
67
|
Persist **entire** session encrypted, including `cookies`, `merchant`, `merchants`, and `switch_credential`. These are credentials, not safe client-side session data.
|
|
66
68
|
|
|
69
|
+
OTP request success and optional `seed` do not prove SMS/WhatsApp delivery. `device_report` means the raw browser SDK report request body, not the `riskToken` returned by its response. No reliable report-format or delivery-status check is available here. Do not retry repeatedly or switch channels without merchant consent.
|
|
70
|
+
|
|
71
|
+
Rails cookie sessions cannot hold full Shopee state (4 KB limit). Use server-side sessions; memory-backed development sessions disappear on restart. Never persist passwords for resend—prompt again when required.
|
|
72
|
+
|
|
67
73
|
```ruby
|
|
68
74
|
serialized = JSON.generate(shopee_session)
|
|
69
75
|
restored = JSON.parse(serialized, symbolize_names: true)
|
|
@@ -136,10 +142,10 @@ Invalid provider envelopes, HTTP failures, expired credentials, malformed inputs
|
|
|
136
142
|
|
|
137
143
|
| Code | Meaning |
|
|
138
144
|
| --- | --- |
|
|
139
|
-
| `10002` |
|
|
145
|
+
| `10002` | Undocumented password-auth response; empty-password probe may continue to OTP, but it does not prove account existence or password state |
|
|
140
146
|
| `200020` | Token invalid or expired — paste a fresh one |
|
|
141
147
|
| `2010000` | Request carried no token |
|
|
142
|
-
| `48401003` | OTP
|
|
148
|
+
| `48401003` | Credential rejected; when returned by OTP verification, the OTP was rejected (reason unspecified) |
|
|
143
149
|
| `48401102` | Password required before an OTP is sent |
|
|
144
150
|
| `48401103` | OTP channel unavailable for this account |
|
|
145
151
|
| `48500102` | Account session expired; log in again with an OTP |
|
|
@@ -156,7 +162,7 @@ gem build rails-shopee-qris.gemspec
|
|
|
156
162
|
Package with `gem build rails-shopee-qris.gemspec`. Authenticate via `gem signin` (or configure a scoped `GEM_HOST_API_KEY` with push permission), then:
|
|
157
163
|
|
|
158
164
|
```sh
|
|
159
|
-
gem push rails-shopee-qris-0.2.
|
|
165
|
+
gem push rails-shopee-qris-0.2.1.gem --host https://rubygems.org
|
|
160
166
|
```
|
|
161
167
|
|
|
162
168
|
RubyGems versions are immutable. Bump the gemspec version before subsequent releases. Built `.gem` files and credentials must not be committed.
|
|
@@ -30,9 +30,9 @@ module Rails
|
|
|
30
30
|
parsed_token, token_meta = parse_token_input(raw_token)
|
|
31
31
|
@token = parsed_token
|
|
32
32
|
@store_id = (store_id || @session&.dig(:store_id) || config.store_id)&.to_s
|
|
33
|
-
@merchant_id = (merchant_id || @session&.dig(:merchant
|
|
33
|
+
@merchant_id = (merchant_id || (@session[:merchant][:id] if @session&.dig(:merchant).is_a?(Hash)) || @session&.dig(:merchant_id) || config.merchant_id)&.to_s
|
|
34
34
|
@phone_number = phone_number || @session&.dig(:phone_number) || config.phone_number
|
|
35
|
-
@device_id = device_id || @session&.dig(:switch_credential
|
|
35
|
+
@device_id = device_id || (@session[:switch_credential][:spc_clientid] if @session&.dig(:switch_credential).is_a?(Hash))
|
|
36
36
|
expiry = expires_at || @session&.dig(:expires_at) || token_meta[:expires_at]
|
|
37
37
|
@expires_at = extract_time(expiry)
|
|
38
38
|
raise Error, "Invalid Shopee session expiry" if expiry && !@expires_at
|
|
@@ -48,7 +48,7 @@ module Rails
|
|
|
48
48
|
raise Error, "Shopee static QRIS is not configured" if static_qris.to_s.empty?
|
|
49
49
|
|
|
50
50
|
code = Qris.generate(static_qris, amount)
|
|
51
|
-
amount = Integer(amount)
|
|
51
|
+
amount = amount.is_a?(String) ? Integer(amount, 10) : Integer(amount)
|
|
52
52
|
{
|
|
53
53
|
qris_id: SecureRandom.hex(16),
|
|
54
54
|
qris_code: code,
|
|
@@ -65,8 +65,7 @@ module Rails
|
|
|
65
65
|
|
|
66
66
|
def refresh!
|
|
67
67
|
unless refreshable?
|
|
68
|
-
raise Error, "Shopee
|
|
69
|
-
"credentials; log in again with OTP or paste a fresh B:... token"
|
|
68
|
+
raise Error, "Shopee session has no renewal credentials; log in again with OTP or paste a fresh B:... token"
|
|
70
69
|
end
|
|
71
70
|
|
|
72
71
|
updated = Setup.new.refresh_session(session)
|
|
@@ -77,12 +76,12 @@ module Rails
|
|
|
77
76
|
end
|
|
78
77
|
|
|
79
78
|
def transactions_between(start_time:, end_time:, store_id: @store_id, page_size: 10, max_pages: 20)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
start_ts = start_time.respond_to?(:to_i) ? start_time.to_i : Integer(start_time)
|
|
83
|
-
end_ts = end_time.respond_to?(:to_i) ? end_time.to_i : Integer(end_time)
|
|
79
|
+
start_ts = transaction_timestamp(start_time)
|
|
80
|
+
end_ts = transaction_timestamp(end_time)
|
|
84
81
|
raise Error, "Shopee transaction time range is invalid" if start_ts > end_ts
|
|
85
82
|
|
|
83
|
+
refresh! if refreshable? && expires_at && expires_at <= Time.now + 900
|
|
84
|
+
|
|
86
85
|
size = [[1, page_size.to_i].max, 10].min
|
|
87
86
|
pages_limit = [1, max_pages.to_i].max
|
|
88
87
|
want_store = store_id&.to_s
|
|
@@ -107,7 +106,8 @@ module Rails
|
|
|
107
106
|
|
|
108
107
|
resp = request_payment(TRANSACTIONS_URL, body)
|
|
109
108
|
data = Response.data(resp)
|
|
110
|
-
list = data[:list]
|
|
109
|
+
list = data[:list]
|
|
110
|
+
raise Error, "Shopee transaction list is missing or invalid" unless list.is_a?(Array)
|
|
111
111
|
|
|
112
112
|
list.each do |raw|
|
|
113
113
|
tx = normalize_transaction(raw, want_store)
|
|
@@ -216,7 +216,8 @@ module Rails
|
|
|
216
216
|
body[:serviceList] = service_list if service_list
|
|
217
217
|
|
|
218
218
|
data = Response.data(request_payment(STORES_URL, body))
|
|
219
|
-
list = data[:list]
|
|
219
|
+
list = data[:list]
|
|
220
|
+
raise Error, "Shopee store list is missing or invalid" unless list.is_a?(Array)
|
|
220
221
|
|
|
221
222
|
list.each do |raw|
|
|
222
223
|
store = normalize_store(raw)
|
|
@@ -322,7 +323,7 @@ module Rails
|
|
|
322
323
|
case obj
|
|
323
324
|
when Hash
|
|
324
325
|
obj.each_with_object({}) do |(k, v), acc|
|
|
325
|
-
acc[k.to_sym] = deep_symbolize(v)
|
|
326
|
+
acc[k.is_a?(String) ? k.to_sym : k] = deep_symbolize(v)
|
|
326
327
|
end
|
|
327
328
|
when Array
|
|
328
329
|
obj.map { |item| deep_symbolize(item) }
|
|
@@ -331,28 +332,25 @@ module Rails
|
|
|
331
332
|
end
|
|
332
333
|
end
|
|
333
334
|
|
|
334
|
-
def
|
|
335
|
-
return
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
expires_at: payload[:exp] ? Time.at(payload[:exp]) : nil
|
|
348
|
-
}
|
|
349
|
-
return [payload[:token].to_s, meta]
|
|
350
|
-
end
|
|
351
|
-
end
|
|
335
|
+
def transaction_timestamp(value)
|
|
336
|
+
return value.to_i if value.is_a?(Time) || (value.is_a?(Numeric) && value.finite?)
|
|
337
|
+
return Integer(value, 10) if value.is_a?(String) && value.match?(/\A[+-]?\d+\z/)
|
|
338
|
+
|
|
339
|
+
raise Error, "Shopee transaction time range is invalid"
|
|
340
|
+
rescue ArgumentError, TypeError, RangeError
|
|
341
|
+
raise Error, "Shopee transaction time range is invalid"
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def parse_token_input(value)
|
|
345
|
+
return [nil, {}] if value.nil?
|
|
346
|
+
raise Error, "Shopee merchant token must be a string" unless value.is_a?(String)
|
|
347
|
+
return [value, {}] if value.start_with?("B:")
|
|
352
348
|
|
|
353
|
-
[
|
|
354
|
-
|
|
355
|
-
|
|
349
|
+
credential = Setup.new.read_merchant_credential([
|
|
350
|
+
{ name: Setup::LIVE_TOKEN_COOKIE, value: value, domain: "partner.shopee.co.id", path: "/" }
|
|
351
|
+
])
|
|
352
|
+
expiry = credential[:expires_at]
|
|
353
|
+
[credential[:token], { expires_at: expiry && Time.at(expiry / 1000.0) }]
|
|
356
354
|
end
|
|
357
355
|
|
|
358
356
|
|
|
@@ -381,7 +379,7 @@ module Rails
|
|
|
381
379
|
end
|
|
382
380
|
|
|
383
381
|
raise Error.new(
|
|
384
|
-
"Shopee rejected the merchant token (code #{code})
|
|
382
|
+
"Shopee rejected the merchant token (code #{code}). " \
|
|
385
383
|
"Log in again with OTP or paste a fresh B:... token.",
|
|
386
384
|
nil, resp[:code], resp
|
|
387
385
|
)
|
|
@@ -389,6 +387,7 @@ module Rails
|
|
|
389
387
|
|
|
390
388
|
err = Response.error(resp)
|
|
391
389
|
raise Error.new(err || "ShopeePay request failed", nil, resp[:code], resp) if err
|
|
390
|
+
Response.data(resp)
|
|
392
391
|
|
|
393
392
|
resp
|
|
394
393
|
end
|
|
@@ -10,7 +10,7 @@ module Rails
|
|
|
10
10
|
unless amount.is_a?(Integer) || (amount.is_a?(String) && amount.match?(/\A\d+\z/))
|
|
11
11
|
raise Error, "Invalid payment amount"
|
|
12
12
|
end
|
|
13
|
-
amount = Integer(amount)
|
|
13
|
+
amount = amount.is_a?(String) ? Integer(amount, 10) : amount
|
|
14
14
|
raise Error, "Payment amount must be greater than zero" unless amount.positive?
|
|
15
15
|
raise Error, "Payment amount exceeds QRIS field length" if amount.to_s.bytesize > 99
|
|
16
16
|
|
|
@@ -9,10 +9,8 @@ module Rails
|
|
|
9
9
|
# Shopee answers many failures with a bare numeric code and no message.
|
|
10
10
|
# Map the ones we have observed so callers get something actionable.
|
|
11
11
|
CODE_HINTS = {
|
|
12
|
-
10002 => "The Shopee account has no password set",
|
|
13
12
|
200020 => "Shopee rejected the token as invalid or expired",
|
|
14
13
|
2010000 => "Shopee request carried no token",
|
|
15
|
-
48401003 => "Shopee rejected the OTP code as wrong or expired",
|
|
16
14
|
48401102 => "Shopee requires the account password before sending an OTP",
|
|
17
15
|
48401103 => "Shopee refused to send the OTP on the requested channel",
|
|
18
16
|
48500102 => "The Shopee account session has expired; log in again with an OTP"
|
|
@@ -37,7 +35,11 @@ module Rails
|
|
|
37
35
|
end
|
|
38
36
|
|
|
39
37
|
code = payload[:code] || payload[:errorCode] || payload[:error]
|
|
40
|
-
code_i = Integer
|
|
38
|
+
code_i = if code.is_a?(Integer)
|
|
39
|
+
code
|
|
40
|
+
elsif code.is_a?(String) && code.match?(/\A-?\d+\z/)
|
|
41
|
+
Integer(code, 10)
|
|
42
|
+
end
|
|
41
43
|
is_success_code = code_i == 0
|
|
42
44
|
|
|
43
45
|
errors = payload[:errors] || inner[:errors]
|
|
@@ -21,9 +21,9 @@ module Rails
|
|
|
21
21
|
CHANNEL_NAMES = { 1 => "SMS", 2 => "voice", 3 => "WhatsApp", 4 => "email", 5 => "Zalo" }.freeze
|
|
22
22
|
NEED_OTP_CODE = 48401102
|
|
23
23
|
NOT_LOGIN_CODE = 48500102
|
|
24
|
-
|
|
24
|
+
EMPTY_PASSWORD_CONTINUATION_CODE = 10002
|
|
25
25
|
OTP_CHANNEL_UNAVAILABLE_CODE = 48401103
|
|
26
|
-
|
|
26
|
+
OTP_REJECTED_CODE = 48401003
|
|
27
27
|
PASSWORD_REQUIRED = "This Shopee account is password-protected; supply the password to receive an OTP"
|
|
28
28
|
|
|
29
29
|
LIVE_TOKEN_COOKIE = "__shopee_partner_website_x_token_live"
|
|
@@ -39,6 +39,9 @@ module Rails
|
|
|
39
39
|
def request_otp(phone_number = Rails::Shopee::Qris.configuration.phone_number, password: nil, channel: nil, device_report: Rails::Shopee::Qris.configuration.device_report)
|
|
40
40
|
phone_info = parse_id_mobile(phone_number)
|
|
41
41
|
raise Error, "Supply a captured Shopee device report to request an OTP" unless device_report.is_a?(String) && !device_report.strip.empty?
|
|
42
|
+
if !channel.nil? && (!channel.is_a?(Integer) || !SEND_OTP_CHANNELS.include?(channel))
|
|
43
|
+
raise Error, "Invalid Shopee OTP channel"
|
|
44
|
+
end
|
|
42
45
|
phone = phone_info[:e164]
|
|
43
46
|
@cookies.clear
|
|
44
47
|
|
|
@@ -61,15 +64,21 @@ module Rails
|
|
|
61
64
|
fingerprint
|
|
62
65
|
)
|
|
63
66
|
|
|
64
|
-
available =
|
|
67
|
+
available = settings[:available_channel_list].nil? ? [] : settings[:available_channel_list]
|
|
68
|
+
unless available.is_a?(Array) && available.all? { |c| c.is_a?(Integer) && SEND_OTP_CHANNELS.include?(c) }
|
|
69
|
+
raise Error, "Shopee returned invalid available OTP channels"
|
|
70
|
+
end
|
|
65
71
|
default_ch = settings[:default_channel]
|
|
66
72
|
resolved = channel || default_ch || DEFAULT_OTP_CHANNEL
|
|
73
|
+
unless resolved.is_a?(Integer) && SEND_OTP_CHANNELS.include?(resolved)
|
|
74
|
+
raise Error, "Shopee returned an invalid OTP channel"
|
|
75
|
+
end
|
|
67
76
|
if !available.empty? && !available.include?(resolved)
|
|
68
77
|
names = available.map { |c| "#{c} (#{CHANNEL_NAMES[c] || 'unknown'})" }.join(", ")
|
|
69
78
|
raise Error, "Shopee OTP channel #{resolved} is unavailable for this account; available: #{names}"
|
|
70
79
|
end
|
|
71
80
|
|
|
72
|
-
begin
|
|
81
|
+
sent = begin
|
|
73
82
|
account_request(
|
|
74
83
|
"/api/v4/account/business/send_otp",
|
|
75
84
|
{
|
|
@@ -86,7 +95,7 @@ module Rails
|
|
|
86
95
|
rescue Error => e
|
|
87
96
|
raise Error.new(
|
|
88
97
|
e.code == OTP_CHANNEL_UNAVAILABLE_CODE ?
|
|
89
|
-
"Shopee refused
|
|
98
|
+
"Shopee refused OTP at /api/v4/account/business/send_otp on channel #{resolved}; choose another available channel" :
|
|
90
99
|
e.message,
|
|
91
100
|
e.status, e.code, e.payload
|
|
92
101
|
)
|
|
@@ -101,6 +110,8 @@ module Rails
|
|
|
101
110
|
risk_token: fingerprint,
|
|
102
111
|
has_password: has_password,
|
|
103
112
|
cookies: snapshot_cookies,
|
|
113
|
+
# Opaque provider field; neither its meaning nor OTP delivery is established.
|
|
114
|
+
seed: sent[:seed],
|
|
104
115
|
requested_at: (Time.now.to_f * 1000).to_i
|
|
105
116
|
}
|
|
106
117
|
end
|
|
@@ -133,10 +144,10 @@ module Rails
|
|
|
133
144
|
fingerprint
|
|
134
145
|
)
|
|
135
146
|
rescue Error => e
|
|
136
|
-
raise e unless e.code.to_s ==
|
|
147
|
+
raise e unless e.code.to_s == OTP_REJECTED_CODE.to_s
|
|
137
148
|
|
|
138
149
|
raise Error.new(
|
|
139
|
-
"Shopee rejected the OTP
|
|
150
|
+
"Shopee rejected the OTP at /api/v4/account/business/verify_otp; cause is not established",
|
|
140
151
|
e.status, e.code, e.payload
|
|
141
152
|
)
|
|
142
153
|
end
|
|
@@ -198,7 +209,7 @@ module Rails
|
|
|
198
209
|
merchant, credential = complete_base(verification, merchant_id)
|
|
199
210
|
profile = get_profile(credential[:token], merchant[:id])
|
|
200
211
|
|
|
201
|
-
data_client = client || Client.new(token: credential[:token])
|
|
212
|
+
data_client = client || Client.new(token: credential[:token], merchant_id: merchant[:id])
|
|
202
213
|
stores = data_client.list_stores
|
|
203
214
|
chosen_store = choose_store_id(stores, store_id, profile[:store_id])
|
|
204
215
|
|
|
@@ -220,7 +231,7 @@ module Rails
|
|
|
220
231
|
},
|
|
221
232
|
profile: profile,
|
|
222
233
|
created_at: (Time.now.to_f * 1000).to_i,
|
|
223
|
-
expires_at: credential[:expires_at] ? Time.at(credential[:expires_at] / 1000) : nil
|
|
234
|
+
expires_at: credential[:expires_at] ? Time.at(credential[:expires_at] / 1000.0) : nil
|
|
224
235
|
}
|
|
225
236
|
end
|
|
226
237
|
|
|
@@ -246,9 +257,9 @@ module Rails
|
|
|
246
257
|
validate_strings(switch_cred, :toc_nonce, :spc_clientid, :device_fingerprint)
|
|
247
258
|
|
|
248
259
|
restore_cookies(session[:cookies] || [])
|
|
249
|
-
alive, payload = login_status
|
|
260
|
+
alive, payload, status = login_status
|
|
250
261
|
unless alive
|
|
251
|
-
raise Error.new("The Shopee account session has expired; log in again with an OTP",
|
|
262
|
+
raise Error.new("The Shopee account session has expired; log in again with an OTP", status, NOT_LOGIN_CODE, payload)
|
|
252
263
|
end
|
|
253
264
|
|
|
254
265
|
verification = {
|
|
@@ -268,7 +279,7 @@ module Rails
|
|
|
268
279
|
session.merge(
|
|
269
280
|
cookies: snapshot_cookies,
|
|
270
281
|
token: credential[:token],
|
|
271
|
-
expires_at: credential[:expires_at] ? Time.at(credential[:expires_at] / 1000) : session[:expires_at]
|
|
282
|
+
expires_at: credential[:expires_at] ? Time.at(credential[:expires_at] / 1000.0) : session[:expires_at]
|
|
272
283
|
)
|
|
273
284
|
end
|
|
274
285
|
|
|
@@ -303,8 +314,8 @@ module Rails
|
|
|
303
314
|
jwt = cookie ? cookie[:value] : nil
|
|
304
315
|
raise Error, "Shopee login did not return a merchant session token" unless jwt.is_a?(String) && !jwt.empty?
|
|
305
316
|
|
|
306
|
-
segments = jwt.split(".")
|
|
307
|
-
raise Error, "Invalid JWT format" unless segments.length == 3 &&
|
|
317
|
+
segments = jwt.split(".", -1)
|
|
318
|
+
raise Error, "Invalid JWT format" unless segments.length == 3 && segments.all? { |segment| segment.match?(/\A[A-Za-z0-9_-]+\z/) }
|
|
308
319
|
|
|
309
320
|
padded = segments[1] + ("=" * (-segments[1].bytesize % 4))
|
|
310
321
|
payload = JSON.parse(Base64.urlsafe_decode64(padded), symbolize_names: true)
|
|
@@ -327,8 +338,12 @@ module Rails
|
|
|
327
338
|
|
|
328
339
|
def symbolize_state(value)
|
|
329
340
|
case value
|
|
330
|
-
when Hash
|
|
341
|
+
when Hash
|
|
342
|
+
raise Error, "Invalid Shopee state keys" unless value.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
|
|
343
|
+
|
|
344
|
+
value.to_h { |key, item| [key.to_sym, symbolize_state(item)] }
|
|
331
345
|
when Array then value.map { |item| symbolize_state(item) }
|
|
346
|
+
when String then value.dup
|
|
332
347
|
else value
|
|
333
348
|
end
|
|
334
349
|
end
|
|
@@ -382,6 +397,10 @@ module Rails
|
|
|
382
397
|
body = { phone: phone, password: hash_shopee_password(password) }
|
|
383
398
|
account_request("/api/v4/account/business/check_account_exist_by_password", body, fingerprint)
|
|
384
399
|
rescue Error => e
|
|
400
|
+
if e.payload.is_a?(Hash)
|
|
401
|
+
data = e.payload[:data]
|
|
402
|
+
raise if e.payload[:captcha_required] == true || data.is_a?(Hash) && data[:captcha_required] == true
|
|
403
|
+
end
|
|
385
404
|
raise unless e.status && (200..299).cover?(e.status) && e.code.is_a?(Integer) && e.code != 0
|
|
386
405
|
|
|
387
406
|
nil
|
|
@@ -398,31 +417,35 @@ module Rails
|
|
|
398
417
|
parsed = parse_json(resp)
|
|
399
418
|
|
|
400
419
|
if parsed[:error] == NEED_OTP_CODE
|
|
401
|
-
|
|
420
|
+
if password.to_s.empty?
|
|
421
|
+
raise Error.new("#{PASSWORD_REQUIRED} (at /api/v4/account/business/authenticate_toc_by_password)", resp.code.to_i, parsed[:error], parsed)
|
|
422
|
+
end
|
|
402
423
|
|
|
403
424
|
return true
|
|
404
425
|
end
|
|
405
426
|
|
|
406
|
-
return false if parsed[:error] == 0
|
|
407
|
-
#
|
|
408
|
-
|
|
427
|
+
return false if parsed[:error] == 0 && parsed[:data].is_a?(Hash)
|
|
428
|
+
# Only this empty-password response was observed to permit OTP requests.
|
|
429
|
+
# Its provider meaning remains unknown; never bypass a supplied password.
|
|
430
|
+
return nil if password.to_s.empty? && parsed[:error] == EMPTY_PASSWORD_CONTINUATION_CODE
|
|
409
431
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
# Any other code means this account does not authenticate by password;
|
|
416
|
-
# OTP delivery is validated by send_otp afterwards.
|
|
417
|
-
false
|
|
432
|
+
raise Error.new(
|
|
433
|
+
"Shopee rejected password authentication at /api/v4/account/business/authenticate_toc_by_password (error #{parsed[:error]})",
|
|
434
|
+
resp.code.to_i, parsed[:error], parsed
|
|
435
|
+
)
|
|
418
436
|
end
|
|
419
437
|
|
|
420
438
|
def login_status
|
|
421
439
|
headers = account_headers.merge("Content-Type" => "application/json")
|
|
422
440
|
resp = send_request(:post, "#{ACCOUNT_BASE_URL}/api/v4/account/business/login_status", headers: headers, body: "{}")
|
|
423
441
|
parsed = parse_json(resp)
|
|
424
|
-
|
|
425
|
-
|
|
442
|
+
unless parsed[:error].is_a?(Integer)
|
|
443
|
+
raise Error.new("Shopee returned a malformed login status at /api/v4/account/business/login_status", resp.code.to_i, parsed[:error], parsed)
|
|
444
|
+
end
|
|
445
|
+
unless [0, NOT_LOGIN_CODE].include?(parsed[:error])
|
|
446
|
+
raise Error.new("Shopee login status failed at /api/v4/account/business/login_status", resp.code.to_i, parsed[:error], parsed)
|
|
447
|
+
end
|
|
448
|
+
[parsed[:error] == 0, parsed, resp.code.to_i]
|
|
426
449
|
end
|
|
427
450
|
|
|
428
451
|
def complete_base(verification, merchant_id)
|
|
@@ -546,7 +569,7 @@ module Rails
|
|
|
546
569
|
|
|
547
570
|
if parsed[:error] != 0 || !parsed[:data].is_a?(Hash)
|
|
548
571
|
err = Response.error(parsed)
|
|
549
|
-
raise Error.new(
|
|
572
|
+
raise Error.new("Shopee account request failed at #{path}: #{err || 'invalid response'}", resp.code.to_i, parsed[:error], parsed)
|
|
550
573
|
end
|
|
551
574
|
|
|
552
575
|
parsed[:data].is_a?(Hash) ? parsed[:data] : {}
|
|
@@ -639,6 +662,9 @@ module Rails
|
|
|
639
662
|
end
|
|
640
663
|
parsed = JSON.parse(response.body.to_s, symbolize_names: true)
|
|
641
664
|
raise Error.new("Shopee returned a malformed JSON envelope", response.code.to_i) unless parsed.is_a?(Hash)
|
|
665
|
+
if parsed[:captcha_required] == true || parsed[:data].is_a?(Hash) && parsed[:data][:captcha_required] == true
|
|
666
|
+
raise Error.new(Response.error(parsed), response.code.to_i, parsed[:error] || parsed[:errorCode] || parsed[:code], parsed)
|
|
667
|
+
end
|
|
642
668
|
|
|
643
669
|
parsed
|
|
644
670
|
rescue JSON::ParserError
|
|
@@ -726,7 +752,7 @@ module Rails
|
|
|
726
752
|
|
|
727
753
|
def snapshot_cookies
|
|
728
754
|
@cookies.delete_if { |_, cookie| cookie[:expires] && cookie[:expires] <= Time.now.to_f }
|
|
729
|
-
@cookies.values
|
|
755
|
+
symbolize_state(@cookies.values)
|
|
730
756
|
end
|
|
731
757
|
|
|
732
758
|
def restore_cookies(cookie_list)
|
|
@@ -744,7 +770,7 @@ module Rails
|
|
|
744
770
|
next if name.match?(/[\s,;]/) || cookie[:value].to_s.match?(/[\r\n;]/)
|
|
745
771
|
next if cookie[:expires] && (!cookie[:expires].is_a?(Numeric) || !cookie[:expires].finite?)
|
|
746
772
|
|
|
747
|
-
@cookies[[name, domain, path]] = cookie.merge(name: name, value: cookie[:value].to_s, domain: domain, path: path)
|
|
773
|
+
@cookies[[name, domain, path]] = symbolize_state(cookie.merge(name: name, value: cookie[:value].to_s, domain: domain, path: path))
|
|
748
774
|
end
|
|
749
775
|
end
|
|
750
776
|
end
|