whop 1.0.0 → 1.0.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: 9645be1e4507621dd2c72c4703f31b5145a9b52ccf5b06069165b58a543730c5
4
- data.tar.gz: 69670ad752cdb9b4135ff02dfae501e3adc01556aa3d24a0b625b36674bcdaa5
3
+ metadata.gz: 1f4c1e5809431db03714e0c34d8c97c18fa1bab3ee8483374634508b6d93d1d7
4
+ data.tar.gz: 92cd8d195acbea810a055b8afc7a55abae6a2640a676af2179f82bf09d108001
5
5
  SHA512:
6
- metadata.gz: 85298153f9224db542ed3b3aac8b1095c47b7992d7705614cdd71958dd0a6ecc9820593cfec65fed539933b5919cee0107f95e0a2eac6a1fe2293c30ad05e470
7
- data.tar.gz: d41161106975b44aeb0d68fcea0ebb8b9bcc8277f0993bc953a5cb43831b8647f9e90e6f3b1b47cbdc39f3337dce634dd8a0bb001a84d551cf5e6d2e580fa418
6
+ metadata.gz: 75986a9c460d6c0a8a041869dd84be895552661619ec9134396d1fcdeba86b8abfdfb347a48f5d1d703a93ba48bfd3c6a84f20758d72c0aacfcdc3fd82a1f9de
7
+ data.tar.gz: 4a668336dc95ac2675026a44beb3a2235d97ae2201bb5172af340e8eb84f7846574de2bc6b0dc0095556aebb7c5c356e2593646dd02523273a2148a772d53422
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
@@ -23,13 +23,16 @@ module Whop
23
23
  private
24
24
 
25
25
  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
26
+ return false unless graphql_result.is_a?(Hash)
27
+ data = graphql_result["data"] || graphql_result
28
+ return false unless data.is_a?(Hash)
29
+
30
+ key = %w[hasAccessToExperience hasAccessToAccessPass hasAccessToCompany].find { |k| data.key?(k) rescue false }
31
+ payload = key ? data[key] : data
32
+
33
+ return payload["hasAccess"] if payload.is_a?(Hash) && payload.key?("hasAccess")
34
+ return payload if payload == true || payload == false
35
+ false
33
36
  end
34
37
  end
35
38
  end
data/lib/whop/client.rb CHANGED
@@ -25,28 +25,56 @@ 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
+ # Simple GraphQL auto-pagination helper.
59
+ # Expects a query that returns { pageInfo: { hasNextPage, endCursor }, nodes: [...] } under a known path.
60
+ # Usage:
61
+ # Whop.client.graphql_each_page("listReceiptsForCompany", { companyId: "biz" }, path: ["company", "receipts"]) { |node| ... }
62
+ def graphql_each_page(operation_name, variables, path:, first: 50, &block)
63
+ raise ArgumentError, "path must be an Array of keys" unless path.is_a?(Array) && !path.empty?
64
+ cursor = nil
65
+ loop do
66
+ page_vars = variables.merge({ first: first })
67
+ page_vars[:after] = cursor if cursor
68
+ data = graphql(operation_name, page_vars)
69
+ segment = dig_hash(data, "data", *path)
70
+ break unless segment.is_a?(Hash)
71
+ nodes = segment["nodes"] || []
72
+ nodes.each { |n| yield n } if block_given?
73
+ page_info = segment["pageInfo"] || {}
74
+ break unless page_info["hasNextPage"]
75
+ cursor = page_info["endCursor"]
48
76
  end
49
- parse_response!(response)
77
+ nil
50
78
  end
51
79
 
52
80
  # Resources
@@ -86,8 +114,9 @@ module Whop
86
114
  def parse_response!(response)
87
115
  body = response.body
88
116
  json = parse_body_safely(body)
89
- if response.status.to_i >= 400
90
- raise Error, "Whop API error (#{response.status}): #{json.inspect}"
117
+ status = response.status.to_i
118
+ if status >= 400
119
+ raise map_status_error(status, json)
91
120
  end
92
121
  json
93
122
  end
@@ -163,13 +192,59 @@ module Whop
163
192
 
164
193
  def extract_access_boolean(graphql_result)
165
194
  # 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")
195
+ return false unless graphql_result.is_a?(Hash)
196
+ data = graphql_result["data"] || graphql_result
197
+ return false unless data.is_a?(Hash)
198
+
199
+ key = %w[hasAccessToExperience hasAccessToAccessPass hasAccessToCompany].find { |k| data.key?(k) rescue false }
200
+ payload = key ? data[key] : data
201
+ return payload["hasAccess"] if payload.is_a?(Hash) && payload.key?("hasAccess")
202
+ return payload if payload == true || payload == false
203
+ false
204
+ end
205
+ end
206
+ end
207
+
208
+ module Whop
209
+ class Client
210
+ private
211
+
212
+ def with_error_mapping
213
+ yield
214
+ rescue Faraday::TimeoutError => e
215
+ raise APITimeoutError.new("Request timed out", cause: e)
216
+ rescue Faraday::ConnectionFailed => e
217
+ raise APIConnectionError.new("Connection failed", cause: e)
218
+ rescue Faraday::SSLError => e
219
+ raise APIConnectionError.new("SSL error", cause: e)
220
+ rescue Faraday::Error => e
221
+ raise APIConnectionError.new(e.message, cause: e)
222
+ end
223
+
224
+ def map_status_error(status, body)
225
+ message = body.is_a?(String) ? body : body.inspect
226
+ case status.to_i
227
+ when 400 then BadRequestError.new(status, message, body: body)
228
+ when 401 then AuthenticationError.new(status, message, body: body)
229
+ when 403 then PermissionDeniedError.new(status, message, body: body)
230
+ when 404 then NotFoundError.new(status, message, body: body)
231
+ when 409 then ConflictError.new(status, message, body: body)
232
+ when 422 then UnprocessableEntityError.new(status, message, body: body)
233
+ when 429 then RateLimitError.new(status, message, body: body)
234
+ else
235
+ if status.to_i >= 500
236
+ InternalServerError.new(status, message, body: body)
237
+ else
238
+ APIStatusError.new(status, message, body: body)
239
+ end
240
+ end
241
+ end
242
+
243
+ def dig_hash(obj, *keys)
244
+ keys.reduce(obj) do |acc, key|
245
+ return nil unless acc.is_a?(Hash)
246
+ acc[key]
171
247
  end
172
- !!graphql_result
173
248
  end
174
249
  end
175
250
  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?
@@ -9,14 +9,80 @@ Whop::DSL.define do
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.1"
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.1
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: