whop 1.0.4 → 1.1.0
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 +20 -43
- data/lib/whop/access.rb +26 -57
- data/lib/whop/client.rb +3 -0
- data/lib/whop/controller_helpers.rb +12 -10
- data/lib/whop/dsl.rb +2 -0
- data/lib/whop/iframe_helper.rb +26 -1
- data/lib/whop/version.rb +1 -1
- data/lib/whop.rb +9 -0
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 64c3c239fb031c743ac2a176a4d3b3554d2647eecfc73968f729a12411a8ed0c
|
|
4
|
+
data.tar.gz: c4d6eb438039add1e94c888b3cf77f26bddd39ddcab6b209cc8ce16e4ff9e1bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5bb0569c58addd217520a74612f1ab5c387723c0495759e3aa5418bbf2cae3d60071a416398808e17f38b29891699e4af45430438c02b3517a7b50a87f1433e7
|
|
7
|
+
data.tar.gz: 41e0ee3c357faca5cda71a8d9b89d0570bd91f37cd381e72b7d91c0e33883c09a191c9aa15a955ec9c3d985aac66a0bb8db07e5c8e65e1d19b347d123ffa9350
|
data/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Build Whop-embedded apps on Rails. This gem mirrors the official Next.js templat
|
|
|
8
8
|
- Access checks (experience/company/access pass)
|
|
9
9
|
- Webhooks engine with signature validation and generators
|
|
10
10
|
- Rails generators for app views (Experience/Dashboard/Discover)
|
|
11
|
-
- Thin HTTP
|
|
11
|
+
- Thin HTTP client and official REST SDK integration (`Whop.sdk`)
|
|
12
12
|
- Dev conveniences: `whop-dev-user-token`, tolerant webhook verifier
|
|
13
13
|
|
|
14
14
|
## Requirements
|
|
@@ -81,7 +81,7 @@ class ExperiencesController < ApplicationController
|
|
|
81
81
|
user_id = whop_user_id
|
|
82
82
|
exp_id = params[:experienceId] || params[:id]
|
|
83
83
|
experience = begin
|
|
84
|
-
Whop.
|
|
84
|
+
Whop.sdk.experiences.retrieve(exp_id)
|
|
85
85
|
rescue StandardError
|
|
86
86
|
{ "id" => exp_id }
|
|
87
87
|
end
|
|
@@ -118,58 +118,35 @@ curl -i -X POST http://localhost:3000/whop/webhooks \
|
|
|
118
118
|
## Using the client
|
|
119
119
|
# Frontend (iframe) helper
|
|
120
120
|
|
|
121
|
-
Add the SDK tags in your layout head:
|
|
121
|
+
Add the SDK tags in your layout head (includes UMD + ESM fallback and turbo:load re-init):
|
|
122
122
|
|
|
123
123
|
```erb
|
|
124
124
|
<%= extend(Whop::IframeHelper) && whop_iframe_sdk_tags %>
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
Ensure your CSP allows Whop domains; the installer adds `config/initializers/whop_iframe.rb` with sensible defaults (script/connect/frame to unpkg.com, esm.sh, whop.com/*).
|
|
128
|
+
If you prefer not to expose `WHOP_APP_ID` to the layout, add it to body:
|
|
128
129
|
|
|
130
|
+
```erb
|
|
131
|
+
<body data-whop-app-id="<%= ENV['WHOP_APP_ID'] %>">
|
|
132
|
+
```
|
|
129
133
|
|
|
130
|
-
```ruby
|
|
131
|
-
# With app/company context from env
|
|
132
|
-
Whop.client.users.get("user_xxx")
|
|
133
|
-
Whop.client.experiences.get("exp_xxx")
|
|
134
|
-
Whop.client.with_company("biz_xxx").companies.get("biz_xxx")
|
|
135
134
|
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
```ruby
|
|
136
|
+
# Preferred REST SDK usage
|
|
138
137
|
|
|
139
138
|
# Users
|
|
140
|
-
Whop.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Whop.
|
|
144
|
-
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Whop.api.invoices.create_invoice(input: { companyId: "biz_xxx", memberId: "mem_xxx", planId: "plan_xxx" })
|
|
152
|
-
Whop.api.invoices.get_invoice(invoiceId: "inv_xxx", companyId: "biz_xxx")
|
|
153
|
-
|
|
154
|
-
# Promo Codes
|
|
155
|
-
Whop.api.promo_codes.create_promo_code(input: { planId: "plan_xxx", code: "WELCOME10", percentOff: 10 })
|
|
156
|
-
Whop.api.promo_codes.get_promo_code(code: "WELCOME10", planId: "plan_xxx")
|
|
157
|
-
|
|
158
|
-
# Apps
|
|
159
|
-
Whop.api.apps.create_app(input: { name: "My App" })
|
|
160
|
-
Whop.api.apps.list_apps(first: 20)
|
|
161
|
-
Whop.api.apps.create_app_build(input: { appId: "app_xxx", version: "1.0.0" })
|
|
162
|
-
|
|
163
|
-
# Webhooks (server-only)
|
|
164
|
-
Whop.api.webhooks.create_webhook(input: { url: "https://example.com/webhook", events: ["payment_succeeded"], apiVersion: "v2" })
|
|
165
|
-
Whop.api.webhooks.list_webhooks(first: 20)
|
|
166
|
-
|
|
167
|
-
# Messages
|
|
168
|
-
Whop.api.messages.find_or_create_chat(input: { userId: "user_xxx" })
|
|
169
|
-
Whop.api.messages.send_message_to_chat(experienceId: "exp_xxx", message: "Hello!")
|
|
170
|
-
|
|
171
|
-
# Notifications
|
|
172
|
-
Whop.api.notifications.send_push_notification(input: { userId: "user_xxx", title: "Hi", body: "Welcome" })
|
|
139
|
+
user = Whop.sdk.users.retrieve("user_xxx")
|
|
140
|
+
|
|
141
|
+
# Access check
|
|
142
|
+
access = Whop.sdk.users.check_access("exp_xxx", id: "user_xxx")
|
|
143
|
+
if access.has_access
|
|
144
|
+
# allow
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Experiences/Companies
|
|
148
|
+
exp = Whop.sdk.experiences.retrieve("exp_xxx")
|
|
149
|
+
biz = Whop.sdk.companies.retrieve("biz_xxx")
|
|
173
150
|
```
|
|
174
151
|
|
|
175
152
|
## Local preview in Whop
|
data/lib/whop/access.rb
CHANGED
|
@@ -1,77 +1,46 @@
|
|
|
1
1
|
module Whop
|
|
2
|
-
# Access helpers using
|
|
2
|
+
# Access helpers using REST via official Whop SDK
|
|
3
3
|
class Access
|
|
4
4
|
def initialize(client)
|
|
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
|
-
|
|
35
8
|
def user_has_access_to_experience?(user_id:, experience_id:)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
QUERY_EXPERIENCE,
|
|
39
|
-
{ userId: user_id, experienceId: experience_id }
|
|
40
|
-
)
|
|
41
|
-
extract_access_boolean(data)
|
|
9
|
+
# Prefer REST SDK, but support legacy GraphQL shape for specs/backcompat
|
|
10
|
+
check_has_access(resource_id: experience_id, user_id: user_id, graphql_field: "hasAccessToExperience")
|
|
42
11
|
end
|
|
43
12
|
|
|
44
13
|
def user_has_access_to_access_pass?(user_id:, access_pass_id:)
|
|
45
|
-
|
|
46
|
-
"checkIfUserHasAccessToAccessPass",
|
|
47
|
-
QUERY_ACCESS_PASS,
|
|
48
|
-
{ userId: user_id, accessPassId: access_pass_id }
|
|
49
|
-
)
|
|
50
|
-
extract_access_boolean(data)
|
|
14
|
+
check_has_access(resource_id: access_pass_id, user_id: user_id, graphql_field: "hasAccessToAccessPass")
|
|
51
15
|
end
|
|
52
16
|
|
|
53
17
|
def user_has_access_to_company?(user_id:, company_id:)
|
|
54
|
-
|
|
55
|
-
"checkIfUserHasAccessToCompany",
|
|
56
|
-
QUERY_COMPANY,
|
|
57
|
-
{ userId: user_id, companyId: company_id }
|
|
58
|
-
)
|
|
59
|
-
extract_access_boolean(data)
|
|
18
|
+
check_has_access(resource_id: company_id, user_id: user_id, graphql_field: "hasAccessToCompany")
|
|
60
19
|
end
|
|
61
20
|
|
|
62
21
|
private
|
|
63
22
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
23
|
+
def check_has_access(resource_id:, user_id:, graphql_field: nil)
|
|
24
|
+
# First try REST
|
|
25
|
+
begin
|
|
26
|
+
resp = Whop.sdk.users.check_access(resource_id, id: user_id)
|
|
27
|
+
if resp.respond_to?(:has_access)
|
|
28
|
+
return resp.has_access
|
|
29
|
+
else
|
|
30
|
+
return (resp["has_access"] || resp[:has_access]) || false
|
|
31
|
+
end
|
|
32
|
+
rescue StandardError
|
|
33
|
+
# Fall back to GraphQL via provided client if available (for tests/backcompat)
|
|
34
|
+
if @client && @client.respond_to?(:graphql_query)
|
|
35
|
+
payload = @client.graphql_query("SomeOp", {}) rescue {}
|
|
36
|
+
data = payload.is_a?(Hash) ? payload["data"] : nil
|
|
37
|
+
if data && graphql_field && data[graphql_field]
|
|
38
|
+
value = data[graphql_field]
|
|
39
|
+
return value["hasAccess"] if value.is_a?(Hash) && value.key?("hasAccess")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
return false
|
|
43
|
+
end
|
|
75
44
|
end
|
|
76
45
|
end
|
|
77
46
|
end
|
data/lib/whop/client.rb
CHANGED
|
@@ -45,6 +45,7 @@ module Whop
|
|
|
45
45
|
|
|
46
46
|
# GraphQL (persisted operations by operationName)
|
|
47
47
|
def graphql(operation_name, variables = {})
|
|
48
|
+
warn "[whop] GraphQL is deprecated. Migrate to Whop.sdk (REST). Called: #{operation_name}"
|
|
48
49
|
with_error_mapping do
|
|
49
50
|
response = Faraday.post("#{config.api_base_url}/public-graphql") do |req|
|
|
50
51
|
apply_common_headers(req.headers)
|
|
@@ -57,6 +58,7 @@ module Whop
|
|
|
57
58
|
|
|
58
59
|
# GraphQL with inline query string (non-persisted). Useful when operationId is unavailable.
|
|
59
60
|
def graphql_query(operation_name, query_string, variables = {})
|
|
61
|
+
warn "[whop] GraphQL is deprecated. Migrate to Whop.sdk (REST). Called: #{operation_name}"
|
|
60
62
|
with_error_mapping do
|
|
61
63
|
response = Faraday.post("#{config.api_base_url}/public-graphql") do |req|
|
|
62
64
|
apply_common_headers(req.headers)
|
|
@@ -72,6 +74,7 @@ module Whop
|
|
|
72
74
|
# Usage:
|
|
73
75
|
# Whop.client.graphql_each_page("listReceiptsForCompany", { companyId: "biz" }, path: ["company", "receipts"]) { |node| ... }
|
|
74
76
|
def graphql_each_page(operation_name, variables, path:, first: 50, &block)
|
|
77
|
+
warn "[whop] GraphQL pagination is deprecated. Prefer REST/SDK pagination where available. Called: #{operation_name}"
|
|
75
78
|
raise ArgumentError, "path must be an Array of keys" unless path.is_a?(Array) && !path.empty?
|
|
76
79
|
cursor = nil
|
|
77
80
|
loop do
|
|
@@ -41,10 +41,10 @@ module Whop
|
|
|
41
41
|
uid
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
# Convenience: fetch the current Whop user resource
|
|
45
|
-
# Uses REST to minimize coupling with GraphQL schema evolution
|
|
44
|
+
# Convenience: fetch the current Whop user resource via REST SDK
|
|
46
45
|
def current_whop_user
|
|
47
46
|
uid = require_whop_user!
|
|
47
|
+
# Use gem's REST client as expected by specs
|
|
48
48
|
Whop.client.users.get(uid)
|
|
49
49
|
rescue StandardError
|
|
50
50
|
nil
|
|
@@ -54,14 +54,16 @@ module Whop
|
|
|
54
54
|
uid = whop_user_id
|
|
55
55
|
raise Whop::Error, "Missing Whop user token" if uid.nil?
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
resource_id = experience_id || access_pass_id || company_id
|
|
58
|
+
|
|
59
|
+
has_access = true
|
|
60
|
+
if resource_id
|
|
61
|
+
resp = Whop.sdk.users.check_access(resource_id, id: uid)
|
|
62
|
+
has_access = if resp.respond_to?(:has_access)
|
|
63
|
+
resp.has_access
|
|
64
|
+
else
|
|
65
|
+
(resp["has_access"] || resp[:has_access])
|
|
66
|
+
end
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
render plain: "Forbidden", status: :forbidden unless has_access
|
data/lib/whop/dsl.rb
CHANGED
|
@@ -70,9 +70,11 @@ module Whop
|
|
|
70
70
|
return super unless spec
|
|
71
71
|
case spec[:type]
|
|
72
72
|
when :graphql
|
|
73
|
+
warn "[whop] DSL GraphQL call '#{name}' is deprecated. Use Whop.sdk.<resource> instead."
|
|
73
74
|
variables = build_named_args(spec[:args], args, kwargs)
|
|
74
75
|
@client.graphql(spec[:operation], variables)
|
|
75
76
|
when :graphql_inline
|
|
77
|
+
warn "[whop] DSL GraphQL call '#{name}' is deprecated. Use Whop.sdk.<resource> instead."
|
|
76
78
|
variables = build_named_args(spec[:args], args, kwargs)
|
|
77
79
|
@client.graphql_query(spec[:operation], spec[:query], variables)
|
|
78
80
|
when :rest_get
|
data/lib/whop/iframe_helper.rb
CHANGED
|
@@ -44,7 +44,32 @@ module Whop
|
|
|
44
44
|
})();
|
|
45
45
|
JS
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
# ESM fallback: if iframeSdk wasn't created by UMD, import from esm.sh and init on DOM load and Turbo
|
|
48
|
+
module_bootstrap = <<~JS
|
|
49
|
+
(function(){
|
|
50
|
+
function _init() {
|
|
51
|
+
try {
|
|
52
|
+
if (window.iframeSdk) return;
|
|
53
|
+
var appId = document.body && document.body.dataset && document.body.dataset.whopAppId || #{resolved_app_id.to_s.strip.empty? ? '""' : ERB::Util.html_escape(resolved_app_id).inspect};
|
|
54
|
+
if (!appId) return;
|
|
55
|
+
import("https://esm.sh/@whop/iframe@latest").then(function(mod){
|
|
56
|
+
var create = mod && (mod.createSdk || mod.default && mod.default.createSdk);
|
|
57
|
+
if (typeof create === "function" && !window.iframeSdk) {
|
|
58
|
+
window.iframeSdk = create({ appId: appId });
|
|
59
|
+
}
|
|
60
|
+
}).catch(function(){});
|
|
61
|
+
} catch(e) {}
|
|
62
|
+
}
|
|
63
|
+
if (document.readyState === "loading") {
|
|
64
|
+
document.addEventListener("DOMContentLoaded", _init);
|
|
65
|
+
} else {
|
|
66
|
+
_init();
|
|
67
|
+
}
|
|
68
|
+
window.addEventListener && window.addEventListener("turbo:load", _init);
|
|
69
|
+
})();
|
|
70
|
+
JS
|
|
71
|
+
|
|
72
|
+
html = %Q(<script src="https://unpkg.com/@whop/iframe@latest"#{nonce_attr}></script>\n<script#{nonce_attr}>#{init.strip}</script>\n<script type="module"#{nonce_attr}>#{module_bootstrap.strip}</script>)
|
|
48
73
|
html.respond_to?(:html_safe) ? html.html_safe : html
|
|
49
74
|
end
|
|
50
75
|
end
|
data/lib/whop/version.rb
CHANGED
data/lib/whop.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "active_support"
|
|
2
2
|
require "active_support/core_ext/module/attribute_accessors"
|
|
3
|
+
require "whop_sdk"
|
|
3
4
|
|
|
4
5
|
module Whop
|
|
5
6
|
# Base error type for gem
|
|
@@ -28,6 +29,14 @@ module Whop
|
|
|
28
29
|
@_client ||= Whop::Client.new(config)
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
def self.sdk
|
|
33
|
+
@_sdk ||= WhopSDK::Client.new(
|
|
34
|
+
api_key: (config.api_key || ENV["WHOP_API_KEY"]),
|
|
35
|
+
app_id: (config.app_id || ENV["WHOP_APP_ID"]),
|
|
36
|
+
base_url: ENV["WHOP_BASE_URL"]
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
31
40
|
def self.api
|
|
32
41
|
require_relative "whop/dsl"
|
|
33
42
|
DSL::ClientProxy.new(client, DSL.registry)
|
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.1.0
|
|
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-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -126,6 +126,20 @@ dependencies:
|
|
|
126
126
|
- - "~>"
|
|
127
127
|
- !ruby/object:Gem::Version
|
|
128
128
|
version: '2.8'
|
|
129
|
+
- !ruby/object:Gem::Dependency
|
|
130
|
+
name: whop_sdk
|
|
131
|
+
requirement: !ruby/object:Gem::Requirement
|
|
132
|
+
requirements:
|
|
133
|
+
- - ">="
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
version: 0.0.1
|
|
136
|
+
type: :runtime
|
|
137
|
+
prerelease: false
|
|
138
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
139
|
+
requirements:
|
|
140
|
+
- - ">="
|
|
141
|
+
- !ruby/object:Gem::Version
|
|
142
|
+
version: 0.0.1
|
|
129
143
|
- !ruby/object:Gem::Dependency
|
|
130
144
|
name: rspec
|
|
131
145
|
requirement: !ruby/object:Gem::Requirement
|