whop 1.0.0 → 1.0.2
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/README.md +139 -17
- data/lib/whop/access.rb +52 -10
- data/lib/whop/client.rb +107 -20
- data/lib/whop/controller_helpers.rb +24 -3
- data/lib/whop/dsl_prelude.rb +69 -3
- data/lib/whop/error.rb +36 -0
- data/lib/whop/version.rb +1 -1
- data/lib/whop.rb +6 -0
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b66a5c1ff726bf0d3142a0d2f3263f990c114f1de42c05dfe3d564d63719e702
|
|
4
|
+
data.tar.gz: c36bc051232dafa141659f0a11df945f5ce06aa8b852ae37504b752d72ed2cfc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 13b4f042216946a03e922f4c7642c6b8a478b3ea95b9c3e727a39e756f989ad7ae6cf248e7dfa74a3101637ea3eef5c9e89acf80d7cd4a322067c0eb4e529cf2
|
|
7
|
+
data.tar.gz: 417ab6ac4239fbe3bbbdccb3a7bf41326e847152318658449fc92fd74e28b2999ac556a8a27656322235eaf74e3ecafe384191a074a1030b15a6fa07ebef72b6
|
data/README.md
CHANGED
|
@@ -1,56 +1,178 @@
|
|
|
1
|
-
# whop
|
|
1
|
+
# whop
|
|
2
2
|
|
|
3
|
-
Rails
|
|
3
|
+
Build Whop-embedded apps on Rails. This gem mirrors the official Next.js template: verify Whop user tokens, gate access to experiences/companies, handle webhooks, scaffold app views, and call Whop APIs.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Highlights
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- Token verification (server-side JWT) and controller helpers
|
|
8
|
+
- Access checks (experience/company/access pass)
|
|
9
|
+
- Webhooks engine with signature validation and generators
|
|
10
|
+
- Rails generators for app views (Experience/Dashboard/Discover)
|
|
11
|
+
- Thin HTTP + GraphQL client, with `with_company`/`with_user` scoping
|
|
12
|
+
- Dev conveniences: `whop-dev-user-token`, tolerant webhook verifier
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Ruby 3.2+
|
|
17
|
+
- Rails 7.0+ (Rails 8 supported)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
1) Add to Gemfile and install
|
|
8
22
|
|
|
9
23
|
```ruby
|
|
10
|
-
gem "whop
|
|
24
|
+
gem "whop", "~> 1.0"
|
|
11
25
|
```
|
|
12
26
|
|
|
13
|
-
Generate initializer and mount webhooks:
|
|
14
|
-
|
|
15
27
|
```bash
|
|
28
|
+
bundle install
|
|
16
29
|
bin/rails g whop:install
|
|
17
30
|
```
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
The installer:
|
|
33
|
+
- Creates `config/initializers/whop.rb` (reads env vars)
|
|
34
|
+
- Mounts `/whop/webhooks`
|
|
35
|
+
- Adds `config/initializers/whop_iframe.rb` so Whop can embed your app (CSP frame-ancestors for `*.whop.com` and removes `X-Frame-Options`).
|
|
36
|
+
|
|
37
|
+
2) Configure environment variables
|
|
20
38
|
|
|
21
39
|
- `WHOP_APP_ID`
|
|
22
40
|
- `WHOP_API_KEY`
|
|
23
41
|
- `WHOP_WEBHOOK_SECRET`
|
|
24
|
-
-
|
|
42
|
+
- Optional: `WHOP_AGENT_USER_ID`, `WHOP_COMPANY_ID`
|
|
25
43
|
|
|
26
|
-
|
|
44
|
+
Tip (dev): use dotenv
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
echo "WHOP_APP_ID=app_xxx
|
|
48
|
+
WHOP_API_KEY=sk_xxx
|
|
49
|
+
WHOP_WEBHOOK_SECRET=whsec_xxx
|
|
50
|
+
WHOP_AGENT_USER_ID=user_xxx
|
|
51
|
+
WHOP_COMPANY_ID=biz_xxx" > .env
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## App Views (routes that Whop calls)
|
|
55
|
+
|
|
56
|
+
Set these in the Whop dashboard (Hosting → App Views):
|
|
57
|
+
|
|
58
|
+
- Experience View: `/experiences/[experienceId]`
|
|
59
|
+
- Dashboard View: `/dashboard/[companyId]`
|
|
60
|
+
- Discover View: `/discover`
|
|
61
|
+
|
|
62
|
+
Generate the pages (dynamic IDs provided by Whop; no args needed):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bin/rails g whop:scaffold:all
|
|
66
|
+
# or individually
|
|
67
|
+
bin/rails g whop:scaffold:company
|
|
68
|
+
bin/rails g whop:scaffold:experience
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The scaffolds include access gating and safe resource fetches (render even if the REST fetch 404s in early dev).
|
|
72
|
+
|
|
73
|
+
## Controller helpers
|
|
27
74
|
|
|
28
75
|
```ruby
|
|
29
76
|
class ExperiencesController < ApplicationController
|
|
30
77
|
include Whop::ControllerHelpers
|
|
31
|
-
before_action -> { require_whop_access!(experience_id: params[:id]) }
|
|
78
|
+
before_action -> { require_whop_access!(experience_id: params[:experienceId] || params[:id]) }
|
|
32
79
|
|
|
33
80
|
def show
|
|
34
81
|
user_id = whop_user_id
|
|
35
|
-
|
|
36
|
-
|
|
82
|
+
exp_id = params[:experienceId] || params[:id]
|
|
83
|
+
experience = begin
|
|
84
|
+
Whop.client.experiences.get(exp_id)
|
|
85
|
+
rescue StandardError
|
|
86
|
+
{ "id" => exp_id }
|
|
87
|
+
end
|
|
88
|
+
render :show, locals: { user_id:, experience: }
|
|
37
89
|
end
|
|
38
90
|
end
|
|
39
91
|
```
|
|
40
92
|
|
|
41
|
-
|
|
93
|
+
Dev token override (development only): send `whop-dev-user-token` as a header or query param; if it looks like a JWT it will be verified, otherwise it is treated as a raw `user_id`.
|
|
94
|
+
|
|
95
|
+
## Webhooks
|
|
96
|
+
|
|
97
|
+
Mounted at `/whop/webhooks`. Signature validation uses `WHOP_WEBHOOK_SECRET`.
|
|
98
|
+
|
|
99
|
+
Generate a handler job:
|
|
42
100
|
|
|
43
101
|
```bash
|
|
44
102
|
bin/rails g whop:webhooks:handler payment_succeeded
|
|
45
|
-
# POST /whop/webhooks -> verifies signature, enqueues Whop::PaymentSucceededJob
|
|
46
103
|
```
|
|
47
104
|
|
|
48
|
-
|
|
105
|
+
Test locally:
|
|
49
106
|
|
|
50
107
|
```bash
|
|
51
|
-
|
|
108
|
+
cat > payload.json <<'JSON'
|
|
109
|
+
{ "action": "payment.succeeded", "data": { "id": "pay_123", "user_id": "user_123", "final_amount": 1000, "amount_after_fees": 950, "currency": "USD" } }
|
|
110
|
+
JSON
|
|
111
|
+
SIG=$(ruby -ropenssl -e 's=ENV.fetch("WHOP_WEBHOOK_SECRET"); p=File.read("payload.json"); puts "sha256=#{OpenSSL::HMAC.hexdigest("SHA256", s, p)}"')
|
|
112
|
+
curl -i -X POST http://localhost:3000/whop/webhooks \
|
|
113
|
+
-H "Content-Type: application/json" \
|
|
114
|
+
-H "X-Whop-Signature: $SIG" \
|
|
115
|
+
--data-binary @payload.json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Using the client
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
# With app/company context from env
|
|
122
|
+
Whop.client.users.get("user_xxx")
|
|
123
|
+
Whop.client.experiences.get("exp_xxx")
|
|
124
|
+
Whop.client.with_company("biz_xxx").companies.get("biz_xxx")
|
|
125
|
+
|
|
126
|
+
# GraphQL (persisted operations)
|
|
127
|
+
Whop.api.access.check_if_user_has_access_to_experience(userId: "user_xxx", experienceId: "exp_xxx")
|
|
128
|
+
|
|
129
|
+
# Users
|
|
130
|
+
Whop.api.users.get_current_user
|
|
131
|
+
Whop.api.users.get_user(userId: "user_xxx")
|
|
132
|
+
Whop.api.users.list_user_socials(userId: "user_xxx", first: 10)
|
|
133
|
+
Whop.api.users.ban_user(input: { userId: "user_xxx", reason: "abuse" })
|
|
134
|
+
|
|
135
|
+
# Payments
|
|
136
|
+
Whop.api.payments.create_checkout_session(input: { planId: "plan_xxx", successUrl: "https://...", cancelUrl: "https://..." })
|
|
137
|
+
Whop.api.payments.charge_user(input: { userId: "user_xxx", amount: 1000, currency: "USD" })
|
|
138
|
+
Whop.api.payments.list_receipts_for_company(companyId: "biz_xxx", first: 20)
|
|
139
|
+
|
|
140
|
+
# Invoices
|
|
141
|
+
Whop.api.invoices.create_invoice(input: { companyId: "biz_xxx", memberId: "mem_xxx", planId: "plan_xxx" })
|
|
142
|
+
Whop.api.invoices.get_invoice(invoiceId: "inv_xxx", companyId: "biz_xxx")
|
|
143
|
+
|
|
144
|
+
# Promo Codes
|
|
145
|
+
Whop.api.promo_codes.create_promo_code(input: { planId: "plan_xxx", code: "WELCOME10", percentOff: 10 })
|
|
146
|
+
Whop.api.promo_codes.get_promo_code(code: "WELCOME10", planId: "plan_xxx")
|
|
147
|
+
|
|
148
|
+
# Apps
|
|
149
|
+
Whop.api.apps.create_app(input: { name: "My App" })
|
|
150
|
+
Whop.api.apps.list_apps(first: 20)
|
|
151
|
+
Whop.api.apps.create_app_build(input: { appId: "app_xxx", version: "1.0.0" })
|
|
152
|
+
|
|
153
|
+
# Webhooks (server-only)
|
|
154
|
+
Whop.api.webhooks.create_webhook(input: { url: "https://example.com/webhook", events: ["payment_succeeded"], apiVersion: "v2" })
|
|
155
|
+
Whop.api.webhooks.list_webhooks(first: 20)
|
|
156
|
+
|
|
157
|
+
# Messages
|
|
158
|
+
Whop.api.messages.find_or_create_chat(input: { userId: "user_xxx" })
|
|
159
|
+
Whop.api.messages.send_message_to_chat(experienceId: "exp_xxx", message: "Hello!")
|
|
160
|
+
|
|
161
|
+
# Notifications
|
|
162
|
+
Whop.api.notifications.send_push_notification(input: { userId: "user_xxx", title: "Hi", body: "Welcome" })
|
|
52
163
|
```
|
|
53
164
|
|
|
165
|
+
## Local preview in Whop
|
|
166
|
+
|
|
167
|
+
- Run Rails: `bin/rails s`
|
|
168
|
+
- In Whop dev tools, set environment to `localhost`
|
|
169
|
+
- For tunneling, add your ngrok domain to `frame_ancestors` in `whop_iframe.rb`
|
|
170
|
+
|
|
171
|
+
## Versioning & support
|
|
172
|
+
|
|
173
|
+
- Add `gem "whop", "~> 1.0"` to stay within v1.x.
|
|
174
|
+
- Issues and PRs welcome.
|
|
175
|
+
|
|
54
176
|
## License
|
|
55
177
|
|
|
56
178
|
MIT
|
data/lib/whop/access.rb
CHANGED
|
@@ -5,31 +5,73 @@ module Whop
|
|
|
5
5
|
@client = client
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
+
QUERY_EXPERIENCE = <<~GRAPHQL
|
|
9
|
+
query checkIfUserHasAccessToExperience($experienceId: ID!, $userId: ID) {
|
|
10
|
+
hasAccessToExperience(experienceId: $experienceId, userId: $userId) {
|
|
11
|
+
hasAccess
|
|
12
|
+
accessLevel
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
GRAPHQL
|
|
16
|
+
|
|
17
|
+
QUERY_ACCESS_PASS = <<~GRAPHQL
|
|
18
|
+
query checkIfUserHasAccessToAccessPass($accessPassId: ID!, $userId: ID) {
|
|
19
|
+
hasAccessToAccessPass(accessPassId: $accessPassId, userId: $userId) {
|
|
20
|
+
hasAccess
|
|
21
|
+
accessLevel
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
GRAPHQL
|
|
25
|
+
|
|
26
|
+
QUERY_COMPANY = <<~GRAPHQL
|
|
27
|
+
query checkIfUserHasAccessToCompany($companyId: ID!, $userId: ID) {
|
|
28
|
+
hasAccessToCompany(companyId: $companyId, userId: $userId) {
|
|
29
|
+
hasAccess
|
|
30
|
+
accessLevel
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
GRAPHQL
|
|
34
|
+
|
|
8
35
|
def user_has_access_to_experience?(user_id:, experience_id:)
|
|
9
|
-
data = @client.
|
|
36
|
+
data = @client.graphql_query(
|
|
37
|
+
"checkIfUserHasAccessToExperience",
|
|
38
|
+
QUERY_EXPERIENCE,
|
|
39
|
+
{ userId: user_id, experienceId: experience_id }
|
|
40
|
+
)
|
|
10
41
|
extract_access_boolean(data)
|
|
11
42
|
end
|
|
12
43
|
|
|
13
44
|
def user_has_access_to_access_pass?(user_id:, access_pass_id:)
|
|
14
|
-
data = @client.
|
|
45
|
+
data = @client.graphql_query(
|
|
46
|
+
"checkIfUserHasAccessToAccessPass",
|
|
47
|
+
QUERY_ACCESS_PASS,
|
|
48
|
+
{ userId: user_id, accessPassId: access_pass_id }
|
|
49
|
+
)
|
|
15
50
|
extract_access_boolean(data)
|
|
16
51
|
end
|
|
17
52
|
|
|
18
53
|
def user_has_access_to_company?(user_id:, company_id:)
|
|
19
|
-
data = @client.
|
|
54
|
+
data = @client.graphql_query(
|
|
55
|
+
"checkIfUserHasAccessToCompany",
|
|
56
|
+
QUERY_COMPANY,
|
|
57
|
+
{ userId: user_id, companyId: company_id }
|
|
58
|
+
)
|
|
20
59
|
extract_access_boolean(data)
|
|
21
60
|
end
|
|
22
61
|
|
|
23
62
|
private
|
|
24
63
|
|
|
25
64
|
def extract_access_boolean(graphql_result)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
65
|
+
return false unless graphql_result.is_a?(Hash)
|
|
66
|
+
data = graphql_result["data"] || graphql_result
|
|
67
|
+
return false unless data.is_a?(Hash)
|
|
68
|
+
|
|
69
|
+
key = %w[hasAccessToExperience hasAccessToAccessPass hasAccessToCompany].find { |k| data.key?(k) rescue false }
|
|
70
|
+
payload = key ? data[key] : data
|
|
71
|
+
|
|
72
|
+
return payload["hasAccess"] if payload.is_a?(Hash) && payload.key?("hasAccess")
|
|
73
|
+
return payload if payload == true || payload == false
|
|
74
|
+
false
|
|
33
75
|
end
|
|
34
76
|
end
|
|
35
77
|
end
|
data/lib/whop/client.rb
CHANGED
|
@@ -25,28 +25,68 @@ module Whop
|
|
|
25
25
|
|
|
26
26
|
# REST helpers
|
|
27
27
|
def get(path, params: nil)
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
with_error_mapping do
|
|
29
|
+
response = connection.get(path) do |req|
|
|
30
|
+
req.params.update(params) if params
|
|
31
|
+
end
|
|
32
|
+
parse_response!(response)
|
|
30
33
|
end
|
|
31
|
-
parse_response!(response)
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def post(path, json: nil)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
with_error_mapping do
|
|
38
|
+
response = connection.post(path) do |req|
|
|
39
|
+
req.headers["Content-Type"] = "application/json"
|
|
40
|
+
req.body = JSON.generate(json) if json
|
|
41
|
+
end
|
|
42
|
+
parse_response!(response)
|
|
38
43
|
end
|
|
39
|
-
parse_response!(response)
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
# GraphQL (persisted operations by operationName)
|
|
43
47
|
def graphql(operation_name, variables = {})
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
with_error_mapping do
|
|
49
|
+
response = Faraday.post("#{config.api_base_url}/public-graphql") do |req|
|
|
50
|
+
apply_common_headers(req.headers)
|
|
51
|
+
req.headers["Content-Type"] = "application/json"
|
|
52
|
+
req.body = JSON.generate({ operationName: operation_name, variables: variables })
|
|
53
|
+
end
|
|
54
|
+
parse_response!(response)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# GraphQL with inline query string (non-persisted). Useful when operationId is unavailable.
|
|
59
|
+
def graphql_query(operation_name, query_string, variables = {})
|
|
60
|
+
with_error_mapping do
|
|
61
|
+
response = Faraday.post("#{config.api_base_url}/public-graphql") do |req|
|
|
62
|
+
apply_common_headers(req.headers)
|
|
63
|
+
req.headers["Content-Type"] = "application/json"
|
|
64
|
+
req.body = JSON.generate({ operationName: operation_name, query: query_string, variables: variables })
|
|
65
|
+
end
|
|
66
|
+
parse_response!(response)
|
|
48
67
|
end
|
|
49
|
-
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Simple GraphQL auto-pagination helper.
|
|
71
|
+
# Expects a query that returns { pageInfo: { hasNextPage, endCursor }, nodes: [...] } under a known path.
|
|
72
|
+
# Usage:
|
|
73
|
+
# Whop.client.graphql_each_page("listReceiptsForCompany", { companyId: "biz" }, path: ["company", "receipts"]) { |node| ... }
|
|
74
|
+
def graphql_each_page(operation_name, variables, path:, first: 50, &block)
|
|
75
|
+
raise ArgumentError, "path must be an Array of keys" unless path.is_a?(Array) && !path.empty?
|
|
76
|
+
cursor = nil
|
|
77
|
+
loop do
|
|
78
|
+
page_vars = variables.merge({ first: first })
|
|
79
|
+
page_vars[:after] = cursor if cursor
|
|
80
|
+
data = graphql(operation_name, page_vars)
|
|
81
|
+
segment = dig_hash(data, "data", *path)
|
|
82
|
+
break unless segment.is_a?(Hash)
|
|
83
|
+
nodes = segment["nodes"] || []
|
|
84
|
+
nodes.each { |n| yield n } if block_given?
|
|
85
|
+
page_info = segment["pageInfo"] || {}
|
|
86
|
+
break unless page_info["hasNextPage"]
|
|
87
|
+
cursor = page_info["endCursor"]
|
|
88
|
+
end
|
|
89
|
+
nil
|
|
50
90
|
end
|
|
51
91
|
|
|
52
92
|
# Resources
|
|
@@ -86,8 +126,9 @@ module Whop
|
|
|
86
126
|
def parse_response!(response)
|
|
87
127
|
body = response.body
|
|
88
128
|
json = parse_body_safely(body)
|
|
89
|
-
|
|
90
|
-
|
|
129
|
+
status = response.status.to_i
|
|
130
|
+
if status >= 400
|
|
131
|
+
raise map_status_error(status, json)
|
|
91
132
|
end
|
|
92
133
|
json
|
|
93
134
|
end
|
|
@@ -163,13 +204,59 @@ module Whop
|
|
|
163
204
|
|
|
164
205
|
def extract_access_boolean(graphql_result)
|
|
165
206
|
# Attempt to locate the access payload; tolerate schema variants
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
207
|
+
return false unless graphql_result.is_a?(Hash)
|
|
208
|
+
data = graphql_result["data"] || graphql_result
|
|
209
|
+
return false unless data.is_a?(Hash)
|
|
210
|
+
|
|
211
|
+
key = %w[hasAccessToExperience hasAccessToAccessPass hasAccessToCompany].find { |k| data.key?(k) rescue false }
|
|
212
|
+
payload = key ? data[key] : data
|
|
213
|
+
return payload["hasAccess"] if payload.is_a?(Hash) && payload.key?("hasAccess")
|
|
214
|
+
return payload if payload == true || payload == false
|
|
215
|
+
false
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
module Whop
|
|
221
|
+
class Client
|
|
222
|
+
private
|
|
223
|
+
|
|
224
|
+
def with_error_mapping
|
|
225
|
+
yield
|
|
226
|
+
rescue Faraday::TimeoutError => e
|
|
227
|
+
raise APITimeoutError.new("Request timed out", cause: e)
|
|
228
|
+
rescue Faraday::ConnectionFailed => e
|
|
229
|
+
raise APIConnectionError.new("Connection failed", cause: e)
|
|
230
|
+
rescue Faraday::SSLError => e
|
|
231
|
+
raise APIConnectionError.new("SSL error", cause: e)
|
|
232
|
+
rescue Faraday::Error => e
|
|
233
|
+
raise APIConnectionError.new(e.message, cause: e)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def map_status_error(status, body)
|
|
237
|
+
message = body.is_a?(String) ? body : body.inspect
|
|
238
|
+
case status.to_i
|
|
239
|
+
when 400 then BadRequestError.new(status, message, body: body)
|
|
240
|
+
when 401 then AuthenticationError.new(status, message, body: body)
|
|
241
|
+
when 403 then PermissionDeniedError.new(status, message, body: body)
|
|
242
|
+
when 404 then NotFoundError.new(status, message, body: body)
|
|
243
|
+
when 409 then ConflictError.new(status, message, body: body)
|
|
244
|
+
when 422 then UnprocessableEntityError.new(status, message, body: body)
|
|
245
|
+
when 429 then RateLimitError.new(status, message, body: body)
|
|
246
|
+
else
|
|
247
|
+
if status.to_i >= 500
|
|
248
|
+
InternalServerError.new(status, message, body: body)
|
|
249
|
+
else
|
|
250
|
+
APIStatusError.new(status, message, body: body)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def dig_hash(obj, *keys)
|
|
256
|
+
keys.reduce(obj) do |acc, key|
|
|
257
|
+
return nil unless acc.is_a?(Hash)
|
|
258
|
+
acc[key]
|
|
171
259
|
end
|
|
172
|
-
!!graphql_result
|
|
173
260
|
end
|
|
174
261
|
end
|
|
175
262
|
end
|
|
@@ -5,7 +5,7 @@ module Whop
|
|
|
5
5
|
def whop_user_id
|
|
6
6
|
# Primary: verified JWT from header
|
|
7
7
|
token = request.headers["x-whop-user-token"] || request.headers["X-Whop-User-Token"]
|
|
8
|
-
if token.
|
|
8
|
+
if token && !token.to_s.empty?
|
|
9
9
|
payload = Whop::Token.verify_from_jwt(token)
|
|
10
10
|
app_id = payload["aud"]
|
|
11
11
|
raise Whop::Error, "Invalid app audience" if app_id != (ENV["WHOP_APP_ID"] || Whop.config.app_id)
|
|
@@ -15,9 +15,9 @@ module Whop
|
|
|
15
15
|
# Development fallback: support whop-dev-user-token (header or param)
|
|
16
16
|
if defined?(Rails) && Rails.env.development?
|
|
17
17
|
dev_token = request.get_header("HTTP_WHOP_DEV_USER_TOKEN") || request.headers["whop-dev-user-token"] || params["whop-dev-user-token"] || params[:whop_dev_user_token]
|
|
18
|
-
if dev_token.
|
|
18
|
+
if dev_token && !dev_token.to_s.empty?
|
|
19
19
|
# If looks like JWT, try to verify; otherwise treat as direct user_id
|
|
20
|
-
if dev_token.include?(".")
|
|
20
|
+
if dev_token.to_s.include?(".")
|
|
21
21
|
payload = Whop::Token.verify_from_jwt(dev_token)
|
|
22
22
|
return payload["sub"]
|
|
23
23
|
else
|
|
@@ -29,6 +29,27 @@ module Whop
|
|
|
29
29
|
nil
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# Alias for readability
|
|
33
|
+
def current_whop_user_id
|
|
34
|
+
whop_user_id
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Ensure a valid Whop user token is present, otherwise raise
|
|
38
|
+
def require_whop_user!
|
|
39
|
+
uid = whop_user_id
|
|
40
|
+
raise Whop::Error, "Missing Whop user token" if uid.nil?
|
|
41
|
+
uid
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Convenience: fetch the current Whop user resource
|
|
45
|
+
# Uses REST to minimize coupling with GraphQL schema evolution
|
|
46
|
+
def current_whop_user
|
|
47
|
+
uid = require_whop_user!
|
|
48
|
+
Whop.client.users.get(uid)
|
|
49
|
+
rescue StandardError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
32
53
|
def require_whop_access!(experience_id: nil, access_pass_id: nil, company_id: nil)
|
|
33
54
|
uid = whop_user_id
|
|
34
55
|
raise Whop::Error, "Missing Whop user token" if uid.nil?
|
data/lib/whop/dsl_prelude.rb
CHANGED
|
@@ -2,21 +2,87 @@ require_relative "dsl"
|
|
|
2
2
|
|
|
3
3
|
Whop::DSL.define do
|
|
4
4
|
resource :access do
|
|
5
|
-
graphql :check_if_user_has_access_to_experience, operation: "
|
|
6
|
-
graphql :check_if_user_has_access_to_access_pass, operation: "
|
|
7
|
-
graphql :check_if_user_has_access_to_company, operation: "
|
|
5
|
+
graphql :check_if_user_has_access_to_experience, operation: "checkIfUserHasAccessToExperience", args: %i[userId experienceId]
|
|
6
|
+
graphql :check_if_user_has_access_to_access_pass, operation: "checkIfUserHasAccessToAccessPass", args: %i[userId accessPassId]
|
|
7
|
+
graphql :check_if_user_has_access_to_company, operation: "checkIfUserHasAccessToCompany", args: %i[userId companyId]
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
resource :users do
|
|
11
11
|
rest_get :get, path: "/v5/users/:userId", args: %i[userId]
|
|
12
|
+
# GraphQL user helpers (mirror TS SDK)
|
|
13
|
+
graphql :get_current_user, operation: "getCurrentUser", args: []
|
|
14
|
+
graphql :get_user, operation: "getUser", args: %i[userId]
|
|
15
|
+
graphql :get_user_ledger_account, operation: "getUserLedgerAccount", args: []
|
|
16
|
+
graphql :list_user_socials, operation: "listUserSocials", args: %i[userId after before first last]
|
|
17
|
+
graphql :ban_user, operation: "banUser", args: %i[input]
|
|
18
|
+
graphql :unban_user, operation: "unbanUser", args: %i[input]
|
|
19
|
+
graphql :mute_user, operation: "muteUser", args: %i[input]
|
|
20
|
+
graphql :unmute_user, operation: "unmuteUser", args: %i[input]
|
|
12
21
|
end
|
|
13
22
|
|
|
14
23
|
resource :experiences do
|
|
15
24
|
rest_get :get, path: "/v5/experiences/:experienceId", args: %i[experienceId]
|
|
25
|
+
graphql :list_experiences, operation: "listExperiences", args: %i[first after]
|
|
26
|
+
graphql :get_experience, operation: "getExperience", args: %i[experienceId]
|
|
16
27
|
end
|
|
17
28
|
|
|
18
29
|
resource :companies do
|
|
19
30
|
rest_get :get, path: "/v5/companies/:companyId", args: %i[companyId]
|
|
31
|
+
graphql :get_company, operation: "getCompany", args: %i[companyId]
|
|
32
|
+
graphql :get_company_ledger_account, operation: "getCompanyLedgerAccount", args: %i[companyId]
|
|
33
|
+
graphql :list_company_members, operation: "listMembers", args: %i[companyId first after]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
resource :payments do
|
|
37
|
+
graphql :create_checkout_session, operation: "createCheckoutSession", args: %i[input]
|
|
38
|
+
graphql :charge_user, operation: "chargeUser", args: %i[input]
|
|
39
|
+
graphql :pay_user, operation: "payUser", args: %i[input]
|
|
40
|
+
graphql :list_receipts_for_company, operation: "listReceiptsForCompany", args: %i[companyId first after filter]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
resource :invoices do
|
|
44
|
+
graphql :create_invoice, operation: "createInvoice", args: %i[input]
|
|
45
|
+
graphql :get_invoice, operation: "getInvoice", args: %i[invoiceId companyId]
|
|
46
|
+
graphql :list_invoices, operation: "listInvoices", args: %i[companyId after before first last]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
resource :promo_codes do
|
|
50
|
+
graphql :create_promo_code, operation: "createPromoCode", args: %i[input]
|
|
51
|
+
graphql :delete_promo_code, operation: "deletePromoCode", args: %i[input]
|
|
52
|
+
graphql :get_promo_code, operation: "getPromoCode", args: %i[code planId]
|
|
53
|
+
graphql :list_promo_codes, operation: "listPromoCodes", args: %i[first after]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
resource :apps do
|
|
57
|
+
graphql :create_app, operation: "createApp", args: %i[input]
|
|
58
|
+
graphql :update_app, operation: "updateApp", args: %i[input]
|
|
59
|
+
graphql :get_app, operation: "getApp", args: %i[appId]
|
|
60
|
+
graphql :list_apps, operation: "listApps", args: %i[first after]
|
|
61
|
+
graphql :create_app_build, operation: "createAppBuild", args: %i[input]
|
|
62
|
+
graphql :promote_app_build, operation: "promoteAppBuild", args: %i[input]
|
|
63
|
+
graphql :unassign_app_build, operation: "unassignAppBuild", args: %i[input]
|
|
64
|
+
graphql :update_app_permissions, operation: "updateAppPermissions", args: %i[input]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
resource :webhooks do
|
|
68
|
+
graphql :create_webhook, operation: "createWebhook", args: %i[input]
|
|
69
|
+
graphql :update_webhook, operation: "updateWebhook", args: %i[input]
|
|
70
|
+
graphql :test_webhook, operation: "testWebhook", args: %i[input]
|
|
71
|
+
graphql :delete_webhook, operation: "deleteWebhook", args: %i[input]
|
|
72
|
+
graphql :get_webhook, operation: "getWebhook", args: %i[id]
|
|
73
|
+
graphql :list_webhooks, operation: "listWebhooks", args: %i[first after]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
resource :messages do
|
|
77
|
+
graphql :find_or_create_chat, operation: "findOrCreateChat", args: %i[input]
|
|
78
|
+
graphql :send_message_to_chat, operation: "sendMessageToChat", args: %i[experienceId message attachments]
|
|
79
|
+
graphql :send_direct_message_to_user, operation: "sendDirectMessageToUser", args: %i[input]
|
|
80
|
+
graphql :list_direct_message_conversations, operation: "listDirectMessageConversations", args: %i[first after]
|
|
81
|
+
graphql :list_messages_from_chat, operation: "listMessagesFromChat", args: %i[experienceId first after]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
resource :notifications do
|
|
85
|
+
graphql :send_push_notification, operation: "sendPushNotification", args: %i[input]
|
|
20
86
|
end
|
|
21
87
|
end
|
|
22
88
|
|
data/lib/whop/error.rb
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
module Whop
|
|
2
2
|
class Error < StandardError; end
|
|
3
|
+
|
|
4
|
+
# Network and timeout errors
|
|
5
|
+
class APIConnectionError < Error
|
|
6
|
+
attr_reader :cause
|
|
7
|
+
def initialize(message = "API connection error", cause: nil)
|
|
8
|
+
super(message)
|
|
9
|
+
@cause = cause
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class APITimeoutError < Error
|
|
14
|
+
attr_reader :cause
|
|
15
|
+
def initialize(message = "API request timed out", cause: nil)
|
|
16
|
+
super(message)
|
|
17
|
+
@cause = cause
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# HTTP status-based errors
|
|
22
|
+
class APIStatusError < Error
|
|
23
|
+
attr_reader :status, :body
|
|
24
|
+
def initialize(status, message = nil, body: nil)
|
|
25
|
+
super(message || "HTTP #{status}")
|
|
26
|
+
@status = status.to_i
|
|
27
|
+
@body = body
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class BadRequestError < APIStatusError; end # 400
|
|
32
|
+
class AuthenticationError < APIStatusError; end # 401
|
|
33
|
+
class PermissionDeniedError < APIStatusError; end # 403
|
|
34
|
+
class NotFoundError < APIStatusError; end # 404
|
|
35
|
+
class ConflictError < APIStatusError; end # 409
|
|
36
|
+
class UnprocessableEntityError < APIStatusError; end # 422
|
|
37
|
+
class RateLimitError < APIStatusError; end # 429
|
|
38
|
+
class InternalServerError < APIStatusError; end # >= 500
|
|
3
39
|
end
|
|
4
40
|
|
|
5
41
|
|
data/lib/whop/version.rb
CHANGED
data/lib/whop.rb
CHANGED
|
@@ -34,6 +34,9 @@ module Whop
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Ensure core client constant is available when requiring the gem
|
|
38
|
+
require_relative "whop/client"
|
|
39
|
+
|
|
37
40
|
if defined?(Rails)
|
|
38
41
|
require_relative "whop/webhooks/engine"
|
|
39
42
|
end
|
|
@@ -48,4 +51,7 @@ require_relative "whop/webhooks/signature"
|
|
|
48
51
|
require_relative "whop/token"
|
|
49
52
|
require_relative "whop/controller_helpers"
|
|
50
53
|
|
|
54
|
+
# Load access helpers (used by specs and controller helpers)
|
|
55
|
+
require_relative "whop/access"
|
|
56
|
+
|
|
51
57
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: whop
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nikhil Nelson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-10-
|
|
11
|
+
date: 2025-10-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -182,7 +182,10 @@ files:
|
|
|
182
182
|
homepage: https://github.com/TheSoloHacker47/whop-gem
|
|
183
183
|
licenses:
|
|
184
184
|
- MIT
|
|
185
|
-
metadata:
|
|
185
|
+
metadata:
|
|
186
|
+
source_code_uri: https://github.com/TheSoloHacker47/whop-gem
|
|
187
|
+
changelog_uri: https://github.com/TheSoloHacker47/whop-gem/blob/main/CHANGELOG.md
|
|
188
|
+
documentation_uri: https://gemdocs.org/gems/whop/
|
|
186
189
|
post_install_message:
|
|
187
190
|
rdoc_options: []
|
|
188
191
|
require_paths:
|