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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9645be1e4507621dd2c72c4703f31b5145a9b52ccf5b06069165b58a543730c5
4
- data.tar.gz: 69670ad752cdb9b4135ff02dfae501e3adc01556aa3d24a0b625b36674bcdaa5
3
+ metadata.gz: b66a5c1ff726bf0d3142a0d2f3263f990c114f1de42c05dfe3d564d63719e702
4
+ data.tar.gz: c36bc051232dafa141659f0a11df945f5ce06aa8b852ae37504b752d72ed2cfc
5
5
  SHA512:
6
- metadata.gz: 85298153f9224db542ed3b3aac8b1095c47b7992d7705614cdd71958dd0a6ecc9820593cfec65fed539933b5919cee0107f95e0a2eac6a1fe2293c30ad05e470
7
- data.tar.gz: d41161106975b44aeb0d68fcea0ebb8b9bcc8277f0993bc953a5cb43831b8647f9e90e6f3b1b47cbdc39f3337dce634dd8a0bb001a84d551cf5e6d2e580fa418
6
+ metadata.gz: 13b4f042216946a03e922f4c7642c6b8a478b3ea95b9c3e727a39e756f989ad7ae6cf248e7dfa74a3101637ea3eef5c9e89acf80d7cd4a322067c0eb4e529cf2
7
+ data.tar.gz: 417ab6ac4239fbe3bbbdccb3a7bf41326e847152318658449fc92fd74e28b2999ac556a8a27656322235eaf74e3ecafe384191a074a1030b15a6fa07ebef72b6
data/README.md CHANGED
@@ -1,56 +1,178 @@
1
- # whop-rails
1
+ # whop
2
2
 
3
- Rails 7+ gem to build embedded Whop apps: token verification, access checks, API client, webhooks, and generators. Mirrors Whop's Next.js app template.
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
- ## Install
5
+ ## Highlights
6
6
 
7
- Add to Gemfile:
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-rails", path: "."
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
- Set env vars:
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
- - (optional) `WHOP_AGENT_USER_ID`, `WHOP_COMPANY_ID`
42
+ - Optional: `WHOP_AGENT_USER_ID`, `WHOP_COMPANY_ID`
25
43
 
26
- ## Usage
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
- experience = Whop.client.experiences.get(params[:id])
36
- render locals: { user_id:, experience: }
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
- Webhooks:
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
- ## Example app template
105
+ Test locally:
49
106
 
50
107
  ```bash
51
- rails new whop_app -m examples/rails_app/template.rb --skip-jbuilder --skip-action-mailbox --skip-action-text --skip-active-storage
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.graphql("CheckIfUserHasAccessToExperience", { userId: user_id, experienceId: experience_id })
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.graphql("CheckIfUserHasAccessToAccessPass", { userId: user_id, accessPassId: access_pass_id })
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.graphql("CheckIfUserHasAccessToCompany", { userId: user_id, companyId: company_id })
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
- if graphql_result.is_a?(Hash)
27
- data = graphql_result["data"] || graphql_result
28
- key = %w[hasAccessToExperience hasAccessToAccessPass hasAccessToCompany].find { |k| data.key?(k) rescue false }
29
- payload = key ? data[key] : data
30
- return payload["hasAccess"] if payload.is_a?(Hash) && payload.key?("hasAccess")
31
- end
32
- !!graphql_result
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
- response = connection.get(path) do |req|
29
- req.params.update(params) if params
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
- response = connection.post(path) do |req|
36
- req.headers["Content-Type"] = "application/json"
37
- req.body = JSON.generate(json) if json
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
- response = Faraday.post("#{config.api_base_url}/public-graphql") do |req|
45
- apply_common_headers(req.headers)
46
- req.headers["Content-Type"] = "application/json"
47
- req.body = JSON.generate({ operationName: operation_name, variables: variables })
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
- parse_response!(response)
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
- if response.status.to_i >= 400
90
- raise Error, "Whop API error (#{response.status}): #{json.inspect}"
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
- if graphql_result.is_a?(Hash)
167
- data = graphql_result["data"] || graphql_result
168
- key = %w[hasAccessToExperience hasAccessToAccessPass hasAccessToCompany].find { |k| data.key?(k) rescue false }
169
- payload = key ? data[key] : data
170
- return payload["hasAccess"] if payload.is_a?(Hash) && payload.key?("hasAccess")
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.present?
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.present?
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?
@@ -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: "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]
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
@@ -1,5 +1,5 @@
1
1
  module Whop
2
- VERSION = "1.0.0"
2
+ VERSION = "1.0.2"
3
3
  end
4
4
 
5
5
 
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.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-17 00:00:00.000000000 Z
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: