whop 1.0.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 +7 -0
- data/README.md +58 -0
- data/config/routes.rb +5 -0
- data/examples/rails_app/template.rb +66 -0
- data/lib/generators/whop/discover_page/discover_page_generator.rb +23 -0
- data/lib/generators/whop/discover_page/templates/discover_controller.rb +6 -0
- data/lib/generators/whop/discover_page/templates/show.html.erb +6 -0
- data/lib/generators/whop/install/install_generator.rb +23 -0
- data/lib/generators/whop/install/templates/whop.rb +10 -0
- data/lib/generators/whop/install/templates/whop_iframe.rb +9 -0
- data/lib/generators/whop/scaffold/all/all_generator.rb +22 -0
- data/lib/generators/whop/scaffold/company/company_generator.rb +26 -0
- data/lib/generators/whop/scaffold/company/templates/companies_controller.rb +17 -0
- data/lib/generators/whop/scaffold/company/templates/show.html.erb +7 -0
- data/lib/generators/whop/scaffold/experience/experience_generator.rb +26 -0
- data/lib/generators/whop/scaffold/experience/templates/experiences_controller.rb +17 -0
- data/lib/generators/whop/scaffold/experience/templates/show.html.erb +7 -0
- data/lib/generators/whop/webhooks/handler/handler_generator.rb +17 -0
- data/lib/generators/whop/webhooks/handler/templates/job.rb +10 -0
- data/lib/generators/whop/webhooks/install/install_generator.rb +15 -0
- data/lib/whop/access.rb +37 -0
- data/lib/whop/client.rb +177 -0
- data/lib/whop/controller_helpers.rb +51 -0
- data/lib/whop/dsl.rb +107 -0
- data/lib/whop/dsl_prelude.rb +23 -0
- data/lib/whop/error.rb +5 -0
- data/lib/whop/token.rb +37 -0
- data/lib/whop/version.rb +5 -0
- data/lib/whop/webhooks/engine.rb +17 -0
- data/lib/whop/webhooks/signature.rb +52 -0
- data/lib/whop.rb +51 -0
- metadata +206 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9645be1e4507621dd2c72c4703f31b5145a9b52ccf5b06069165b58a543730c5
|
|
4
|
+
data.tar.gz: 69670ad752cdb9b4135ff02dfae501e3adc01556aa3d24a0b625b36674bcdaa5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 85298153f9224db542ed3b3aac8b1095c47b7992d7705614cdd71958dd0a6ecc9820593cfec65fed539933b5919cee0107f95e0a2eac6a1fe2293c30ad05e470
|
|
7
|
+
data.tar.gz: d41161106975b44aeb0d68fcea0ebb8b9bcc8277f0993bc953a5cb43831b8647f9e90e6f3b1b47cbdc39f3337dce634dd8a0bb001a84d551cf5e6d2e580fa418
|
data/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# whop-rails
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Add to Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "whop-rails", path: "."
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Generate initializer and mount webhooks:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bin/rails g whop:install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Set env vars:
|
|
20
|
+
|
|
21
|
+
- `WHOP_APP_ID`
|
|
22
|
+
- `WHOP_API_KEY`
|
|
23
|
+
- `WHOP_WEBHOOK_SECRET`
|
|
24
|
+
- (optional) `WHOP_AGENT_USER_ID`, `WHOP_COMPANY_ID`
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
class ExperiencesController < ApplicationController
|
|
30
|
+
include Whop::ControllerHelpers
|
|
31
|
+
before_action -> { require_whop_access!(experience_id: params[:id]) }
|
|
32
|
+
|
|
33
|
+
def show
|
|
34
|
+
user_id = whop_user_id
|
|
35
|
+
experience = Whop.client.experiences.get(params[:id])
|
|
36
|
+
render locals: { user_id:, experience: }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Webhooks:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bin/rails g whop:webhooks:handler payment_succeeded
|
|
45
|
+
# POST /whop/webhooks -> verifies signature, enqueues Whop::PaymentSucceededJob
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Example app template
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
rails new whop_app -m examples/rails_app/template.rb --skip-jbuilder --skip-action-mailbox --skip-action-text --skip-active-storage
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
|
57
|
+
|
|
58
|
+
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rails application template for a Whop-enabled embedded app.
|
|
4
|
+
# Usage:
|
|
5
|
+
# rails new whop_app -m examples/rails_app/template.rb --skip-jbuilder --skip-action-mailbox --skip-action-text --skip-active-storage
|
|
6
|
+
|
|
7
|
+
say "Adding whop-rails gem..."
|
|
8
|
+
append_to_file "Gemfile", <<~RUBY
|
|
9
|
+
|
|
10
|
+
gem "whop-rails", path: File.expand_path("../../", __dir__)
|
|
11
|
+
RUBY
|
|
12
|
+
|
|
13
|
+
run "bundle install"
|
|
14
|
+
|
|
15
|
+
say "Installing Whop initializer and webhooks engine..."
|
|
16
|
+
generate "whop:install"
|
|
17
|
+
|
|
18
|
+
env_keys = %w[WHOP_APP_ID WHOP_API_KEY WHOP_WEBHOOK_SECRET]
|
|
19
|
+
say "Remember to set ENV: #{env_keys.join(', ')}", :yellow
|
|
20
|
+
|
|
21
|
+
say "Adding ExperiencesController and route..."
|
|
22
|
+
create_file "app/controllers/experiences_controller.rb", <<~RUBY
|
|
23
|
+
class ExperiencesController < ApplicationController
|
|
24
|
+
include Whop::ControllerHelpers
|
|
25
|
+
before_action -> { require_whop_access!(experience_id: params[:id]) }
|
|
26
|
+
|
|
27
|
+
def show
|
|
28
|
+
user_id = whop_user_id
|
|
29
|
+
experience = Whop.client.experiences.get(params[:id])
|
|
30
|
+
render :show, locals: { user_id: user_id, experience: experience }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
RUBY
|
|
34
|
+
|
|
35
|
+
create_file "app/views/experiences/show.html.erb", <<~ERB
|
|
36
|
+
<div class="container">
|
|
37
|
+
<h1>Experience</h1>
|
|
38
|
+
<p>User ID: <%= user_id %></p>
|
|
39
|
+
<pre><%= JSON.pretty_generate(experience) %></pre>
|
|
40
|
+
</div>
|
|
41
|
+
ERB
|
|
42
|
+
|
|
43
|
+
route "resources :experiences, only: [:show]"
|
|
44
|
+
|
|
45
|
+
say "Adding Discover page..."
|
|
46
|
+
create_file "app/controllers/discover_controller.rb", <<~RUBY
|
|
47
|
+
class DiscoverController < ApplicationController
|
|
48
|
+
def show; end
|
|
49
|
+
end
|
|
50
|
+
RUBY
|
|
51
|
+
|
|
52
|
+
create_file "app/views/discover/show.html.erb", <<~ERB
|
|
53
|
+
<div class="container">
|
|
54
|
+
<h1>Discover your app</h1>
|
|
55
|
+
<p>Showcase value, link to communities, add referral params.</p>
|
|
56
|
+
</div>
|
|
57
|
+
ERB
|
|
58
|
+
|
|
59
|
+
route "get '/discover', to: 'discover#show'"
|
|
60
|
+
|
|
61
|
+
say "Generating example webhook handler..."
|
|
62
|
+
generate "whop:webhooks:handler", "payment_succeeded"
|
|
63
|
+
|
|
64
|
+
say "All set. Configure ENV and run: bin/rails server"
|
|
65
|
+
|
|
66
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module Whop
|
|
4
|
+
module Generators
|
|
5
|
+
class DiscoverPageGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
def create_controller
|
|
9
|
+
template "discover_controller.rb", "app/controllers/discover_controller.rb"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add_route
|
|
13
|
+
route "get '/discover', to: 'discover#show'"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_view
|
|
17
|
+
template "show.html.erb", "app/views/discover/show.html.erb"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module Whop
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
def create_initializer
|
|
9
|
+
template "whop.rb", "config/initializers/whop.rb"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def mount_engine
|
|
13
|
+
route "mount Whop::Webhooks::Engine => '/whop/webhooks'"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_iframe_initializer
|
|
17
|
+
template "whop_iframe.rb", "config/initializers/whop_iframe.rb"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Whop.configure do |config|
|
|
2
|
+
config.app_id = ENV["WHOP_APP_ID"]
|
|
3
|
+
config.api_key = ENV["WHOP_API_KEY"]
|
|
4
|
+
config.webhook_secret = ENV["WHOP_WEBHOOK_SECRET"]
|
|
5
|
+
config.agent_user_id = ENV["WHOP_AGENT_USER_ID"]
|
|
6
|
+
config.company_id = ENV["WHOP_COMPANY_ID"]
|
|
7
|
+
# config.api_base_url = "https://api.whop.com"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Allow Whop to embed this app in an iframe
|
|
2
|
+
|
|
3
|
+
Rails.application.config.action_dispatch.default_headers.delete('X-Frame-Options')
|
|
4
|
+
|
|
5
|
+
Rails.application.config.content_security_policy do |policy|
|
|
6
|
+
policy.frame_ancestors :self, "https://whop.com", "https://*.whop.com"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module Whop
|
|
4
|
+
module Scaffold
|
|
5
|
+
module Generators
|
|
6
|
+
class AllGenerator < Rails::Generators::Base
|
|
7
|
+
argument :company_id, type: :string, required: false, default: nil, desc: "(optional) Whop Company ID"
|
|
8
|
+
argument :experience_id, type: :string, required: false, default: nil, desc: "(optional) Whop Experience ID"
|
|
9
|
+
|
|
10
|
+
def scaffold_company
|
|
11
|
+
invoke "whop:scaffold:company", [company_id].compact
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def scaffold_experience
|
|
15
|
+
invoke "whop:scaffold:experience", [experience_id].compact
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module Whop
|
|
4
|
+
module Scaffold
|
|
5
|
+
module Generators
|
|
6
|
+
class CompanyGenerator < Rails::Generators::Base
|
|
7
|
+
argument :company_id, type: :string, required: false, default: nil, desc: "(optional) Whop Company ID (not required)"
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
def create_controller
|
|
11
|
+
template "companies_controller.rb", "app/controllers/companies_controller.rb"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add_route
|
|
15
|
+
route "get '/dashboard/:companyId', to: 'companies#show', as: :dashboard_company"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create_view
|
|
19
|
+
template "show.html.erb", "app/views/companies/show.html.erb"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CompaniesController < ApplicationController
|
|
2
|
+
include Whop::ControllerHelpers
|
|
3
|
+
before_action -> { require_whop_access!(company_id: params[:companyId] || params[:id]) }
|
|
4
|
+
|
|
5
|
+
def show
|
|
6
|
+
user_id = whop_user_id
|
|
7
|
+
company_id = params[:companyId] || params[:id]
|
|
8
|
+
company = begin
|
|
9
|
+
Whop.client.with_company(company_id).companies.get(company_id)
|
|
10
|
+
rescue StandardError
|
|
11
|
+
{ "id" => company_id }
|
|
12
|
+
end
|
|
13
|
+
render :show, locals: { user_id:, company: }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module Whop
|
|
4
|
+
module Scaffold
|
|
5
|
+
module Generators
|
|
6
|
+
class ExperienceGenerator < Rails::Generators::Base
|
|
7
|
+
argument :experience_id, type: :string, required: false, default: nil, desc: "(optional) Whop Experience ID (not required)"
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
def create_controller
|
|
11
|
+
template "experiences_controller.rb", "app/controllers/experiences_controller.rb"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add_route
|
|
15
|
+
route "get '/experiences/:experienceId', to: 'experiences#show', as: :experience"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create_view
|
|
19
|
+
template "show.html.erb", "app/views/experiences/show.html.erb"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class ExperiencesController < ApplicationController
|
|
2
|
+
include Whop::ControllerHelpers
|
|
3
|
+
before_action -> { require_whop_access!(experience_id: params[:experienceId] || params[:id]) }
|
|
4
|
+
|
|
5
|
+
def show
|
|
6
|
+
user_id = whop_user_id
|
|
7
|
+
exp_id = params[:experienceId] || params[:id]
|
|
8
|
+
experience = begin
|
|
9
|
+
Whop.client.experiences.get(exp_id)
|
|
10
|
+
rescue StandardError
|
|
11
|
+
{ "id" => exp_id }
|
|
12
|
+
end
|
|
13
|
+
render :show, locals: { user_id:, experience: }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module Whop
|
|
4
|
+
module Webhooks
|
|
5
|
+
module Generators
|
|
6
|
+
class HandlerGenerator < Rails::Generators::NamedBase
|
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
|
8
|
+
|
|
9
|
+
def create_job
|
|
10
|
+
template "job.rb", File.join("app/jobs/whop", "#{file_name}_job.rb")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
|
data/lib/whop/access.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Whop
|
|
2
|
+
# Access helpers using persisted GraphQL operations per Whop docs
|
|
3
|
+
class Access
|
|
4
|
+
def initialize(client)
|
|
5
|
+
@client = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def user_has_access_to_experience?(user_id:, experience_id:)
|
|
9
|
+
data = @client.graphql("CheckIfUserHasAccessToExperience", { userId: user_id, experienceId: experience_id })
|
|
10
|
+
extract_access_boolean(data)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def user_has_access_to_access_pass?(user_id:, access_pass_id:)
|
|
14
|
+
data = @client.graphql("CheckIfUserHasAccessToAccessPass", { userId: user_id, accessPassId: access_pass_id })
|
|
15
|
+
extract_access_boolean(data)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def user_has_access_to_company?(user_id:, company_id:)
|
|
19
|
+
data = @client.graphql("CheckIfUserHasAccessToCompany", { userId: user_id, companyId: company_id })
|
|
20
|
+
extract_access_boolean(data)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
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
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
|
data/lib/whop/client.rb
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "json"
|
|
3
|
+
require "faraday/retry"
|
|
4
|
+
|
|
5
|
+
module Whop
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Thin HTTP client for Whop API + GraphQL with context headers.
|
|
9
|
+
class Client
|
|
10
|
+
attr_reader :config, :on_behalf_of_user_id, :company_id
|
|
11
|
+
|
|
12
|
+
def initialize(config, on_behalf_of_user_id: nil, company_id: nil)
|
|
13
|
+
@config = config
|
|
14
|
+
@on_behalf_of_user_id = on_behalf_of_user_id || config.agent_user_id
|
|
15
|
+
@company_id = company_id || config.company_id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def with_user(user_id)
|
|
19
|
+
self.class.new(config, on_behalf_of_user_id: user_id, company_id: @company_id)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def with_company(company_id)
|
|
23
|
+
self.class.new(config, on_behalf_of_user_id: @on_behalf_of_user_id, company_id: company_id)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# REST helpers
|
|
27
|
+
def get(path, params: nil)
|
|
28
|
+
response = connection.get(path) do |req|
|
|
29
|
+
req.params.update(params) if params
|
|
30
|
+
end
|
|
31
|
+
parse_response!(response)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
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
|
|
38
|
+
end
|
|
39
|
+
parse_response!(response)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# GraphQL (persisted operations by operationName)
|
|
43
|
+
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
|
+
end
|
|
49
|
+
parse_response!(response)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Resources
|
|
53
|
+
def users
|
|
54
|
+
@users ||= Resources::Users.new(self)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def experiences
|
|
58
|
+
@experiences ||= Resources::Experiences.new(self)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def companies
|
|
62
|
+
@companies ||= Resources::Companies.new(self)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def access
|
|
66
|
+
require_relative "access"
|
|
67
|
+
@access ||= Access.new(self)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def connection
|
|
73
|
+
@connection ||= Faraday.new(url: config.api_base_url) do |faraday|
|
|
74
|
+
faraday.request :retry, max: 2, interval: 0.1, backoff_factor: 2
|
|
75
|
+
faraday.response :raise_error
|
|
76
|
+
faraday.adapter Faraday.default_adapter
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def apply_common_headers(headers)
|
|
81
|
+
headers["Authorization"] = "Bearer #{config.api_key}"
|
|
82
|
+
headers["x-on-behalf-of"] = on_behalf_of_user_id if on_behalf_of_user_id
|
|
83
|
+
headers["x-company-id"] = company_id if company_id
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def parse_response!(response)
|
|
87
|
+
body = response.body
|
|
88
|
+
json = parse_body_safely(body)
|
|
89
|
+
if response.status.to_i >= 400
|
|
90
|
+
raise Error, "Whop API error (#{response.status}): #{json.inspect}"
|
|
91
|
+
end
|
|
92
|
+
json
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def parse_body_safely(body)
|
|
96
|
+
return body unless body.is_a?(String) && !body.empty?
|
|
97
|
+
JSON.parse(body)
|
|
98
|
+
rescue JSON::ParserError
|
|
99
|
+
body
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
module Whop
|
|
105
|
+
module Resources
|
|
106
|
+
class Base
|
|
107
|
+
attr_reader :client
|
|
108
|
+
def initialize(client)
|
|
109
|
+
@client = client
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class Users < Base
|
|
114
|
+
def get(user_id)
|
|
115
|
+
client.get("/v5/users/#{user_id}")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class Experiences < Base
|
|
120
|
+
def get(experience_id)
|
|
121
|
+
client.get("/v5/experiences/#{experience_id}")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class Companies < Base
|
|
126
|
+
def get(company_id)
|
|
127
|
+
# If the client is already scoped to this company, use the context-aware endpoint
|
|
128
|
+
if client.company_id && client.company_id == company_id
|
|
129
|
+
# Whop v5 exposes a context-aware company endpoint that reads x-company-id
|
|
130
|
+
return client.get("/v5/company")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Otherwise, fetch via app-scoped companies endpoint by id
|
|
134
|
+
client.get("/v5/app/companies/#{company_id}")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
module Whop
|
|
141
|
+
# Access helpers using persisted GraphQL operations per Whop docs
|
|
142
|
+
class Access
|
|
143
|
+
def initialize(client)
|
|
144
|
+
@client = client
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def user_has_access_to_experience?(user_id:, experience_id:)
|
|
148
|
+
data = @client.graphql("CheckIfUserHasAccessToExperience", { userId: user_id, experienceId: experience_id })
|
|
149
|
+
extract_access_boolean(data)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def user_has_access_to_access_pass?(user_id:, access_pass_id:)
|
|
153
|
+
data = @client.graphql("CheckIfUserHasAccessToAccessPass", { userId: user_id, accessPassId: access_pass_id })
|
|
154
|
+
extract_access_boolean(data)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def user_has_access_to_company?(user_id:, company_id:)
|
|
158
|
+
data = @client.graphql("CheckIfUserHasAccessToCompany", { userId: user_id, companyId: company_id })
|
|
159
|
+
extract_access_boolean(data)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def extract_access_boolean(graphql_result)
|
|
165
|
+
# 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")
|
|
171
|
+
end
|
|
172
|
+
!!graphql_result
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Whop
|
|
2
|
+
module ControllerHelpers
|
|
3
|
+
private
|
|
4
|
+
|
|
5
|
+
def whop_user_id
|
|
6
|
+
# Primary: verified JWT from header
|
|
7
|
+
token = request.headers["x-whop-user-token"] || request.headers["X-Whop-User-Token"]
|
|
8
|
+
if token.present?
|
|
9
|
+
payload = Whop::Token.verify_from_jwt(token)
|
|
10
|
+
app_id = payload["aud"]
|
|
11
|
+
raise Whop::Error, "Invalid app audience" if app_id != (ENV["WHOP_APP_ID"] || Whop.config.app_id)
|
|
12
|
+
return payload["sub"]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Development fallback: support whop-dev-user-token (header or param)
|
|
16
|
+
if defined?(Rails) && Rails.env.development?
|
|
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?
|
|
19
|
+
# If looks like JWT, try to verify; otherwise treat as direct user_id
|
|
20
|
+
if dev_token.include?(".")
|
|
21
|
+
payload = Whop::Token.verify_from_jwt(dev_token)
|
|
22
|
+
return payload["sub"]
|
|
23
|
+
else
|
|
24
|
+
return dev_token
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def require_whop_access!(experience_id: nil, access_pass_id: nil, company_id: nil)
|
|
33
|
+
uid = whop_user_id
|
|
34
|
+
raise Whop::Error, "Missing Whop user token" if uid.nil?
|
|
35
|
+
|
|
36
|
+
has_access = if experience_id
|
|
37
|
+
Whop.client.access.user_has_access_to_experience?(user_id: uid, experience_id: experience_id)
|
|
38
|
+
elsif access_pass_id
|
|
39
|
+
Whop.client.access.user_has_access_to_access_pass?(user_id: uid, access_pass_id: access_pass_id)
|
|
40
|
+
elsif company_id
|
|
41
|
+
Whop.client.access.user_has_access_to_company?(user_id: uid, company_id: company_id)
|
|
42
|
+
else
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
render plain: "Forbidden", status: :forbidden unless has_access
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
|
data/lib/whop/dsl.rb
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module Whop
|
|
2
|
+
module DSL
|
|
3
|
+
class Registry
|
|
4
|
+
attr_reader :resources
|
|
5
|
+
def initialize
|
|
6
|
+
@resources = {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def resource(name, &block)
|
|
10
|
+
ns = (@resources[name.to_sym] ||= Namespace.new(name))
|
|
11
|
+
ns.instance_eval(&block) if block
|
|
12
|
+
ns
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Namespace
|
|
17
|
+
attr_reader :name, :methods
|
|
18
|
+
def initialize(name)
|
|
19
|
+
@name = name.to_sym
|
|
20
|
+
@methods = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def graphql(method_name, operation:, args: [])
|
|
24
|
+
@methods[method_name.to_sym] = { type: :graphql, operation: operation, args: Array(args).map(&:to_sym) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def rest_get(method_name, path:, args: [], params: [])
|
|
28
|
+
@methods[method_name.to_sym] = { type: :rest_get, path: path, args: Array(args).map(&:to_sym), params: Array(params).map(&:to_sym) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def rest_post(method_name, path:, args: [], body: [])
|
|
32
|
+
@methods[method_name.to_sym] = { type: :rest_post, path: path, args: Array(args).map(&:to_sym), body: Array(body).map(&:to_sym) }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class ClientProxy
|
|
37
|
+
def initialize(client, registry)
|
|
38
|
+
@client = client
|
|
39
|
+
@registry = registry
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
43
|
+
ns = @registry.resources[name.to_sym]
|
|
44
|
+
return super unless ns
|
|
45
|
+
NamespaceProxy.new(@client, ns)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def respond_to_missing?(name, include_all = false)
|
|
49
|
+
@registry.resources.key?(name.to_sym) || super
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class NamespaceProxy
|
|
54
|
+
def initialize(client, namespace)
|
|
55
|
+
@client = client
|
|
56
|
+
@namespace = namespace
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
60
|
+
spec = @namespace.methods[name.to_sym]
|
|
61
|
+
return super unless spec
|
|
62
|
+
case spec[:type]
|
|
63
|
+
when :graphql
|
|
64
|
+
variables = build_named_args(spec[:args], args, kwargs)
|
|
65
|
+
@client.graphql(spec[:operation], variables)
|
|
66
|
+
when :rest_get
|
|
67
|
+
path = interpolate_path(spec[:path], build_named_args(spec[:args], args, kwargs))
|
|
68
|
+
query = kwargs.select { |k, _| spec[:params].include?(k.to_sym) }
|
|
69
|
+
@client.get(path, params: query)
|
|
70
|
+
when :rest_post
|
|
71
|
+
path = interpolate_path(spec[:path], build_named_args(spec[:args], args, kwargs))
|
|
72
|
+
body = kwargs.select { |k, _| spec[:body].include?(k.to_sym) }
|
|
73
|
+
@client.post(path, json: body)
|
|
74
|
+
else
|
|
75
|
+
raise Whop::Error, "Unknown DSL method type: #{spec[:type]}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def respond_to_missing?(name, include_all = false)
|
|
80
|
+
@namespace.methods.key?(name.to_sym) || super
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def build_named_args(arg_names, args, kwargs)
|
|
86
|
+
return kwargs if kwargs && !kwargs.empty?
|
|
87
|
+
Hash[arg_names.zip(args)]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def interpolate_path(path, named)
|
|
91
|
+
path.gsub(/:(\w+)/) { |m| named[$1.to_sym] }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
module_function
|
|
96
|
+
|
|
97
|
+
def registry
|
|
98
|
+
@registry ||= Registry.new
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def define(&block)
|
|
102
|
+
registry.instance_eval(&block)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require_relative "dsl"
|
|
2
|
+
|
|
3
|
+
Whop::DSL.define do
|
|
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]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
resource :users do
|
|
11
|
+
rest_get :get, path: "/v5/users/:userId", args: %i[userId]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
resource :experiences do
|
|
15
|
+
rest_get :get, path: "/v5/experiences/:experienceId", args: %i[experienceId]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
resource :companies do
|
|
19
|
+
rest_get :get, path: "/v5/companies/:companyId", args: %i[companyId]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
|
data/lib/whop/error.rb
ADDED
data/lib/whop/token.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "jwt"
|
|
2
|
+
require "openssl"
|
|
3
|
+
|
|
4
|
+
module Whop
|
|
5
|
+
module Token
|
|
6
|
+
JWT_PEM = <<~PEM.freeze
|
|
7
|
+
-----BEGIN PUBLIC KEY-----
|
|
8
|
+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErz8a8vxvexHC0TLT91g7llOdDOsN
|
|
9
|
+
uYiGEfic4Qhni+HMfRBuUphOh7F3k8QgwZc9UlL0AHmyYqtbhL9NuJes6w==
|
|
10
|
+
-----END PUBLIC KEY-----
|
|
11
|
+
PEM
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def verify(headers)
|
|
16
|
+
token = headers["x-whop-user-token"] || headers["X-Whop-User-Token"]
|
|
17
|
+
raise Whop::Error, "Missing x-whop-user-token header" if token.nil? || token.empty?
|
|
18
|
+
payload = verify_from_jwt(token)
|
|
19
|
+
app_id = payload["aud"]
|
|
20
|
+
expected = ENV["WHOP_APP_ID"] || Whop.config.app_id
|
|
21
|
+
raise Whop::Error, "Token audience mismatch" if expected && app_id != expected
|
|
22
|
+
{ "user_id" => payload["sub"] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def verify_from_jwt(token)
|
|
26
|
+
key = OpenSSL::PKey::EC.new(JWT_PEM)
|
|
27
|
+
payload, _header = JWT.decode(token, key, true, {
|
|
28
|
+
iss: "urn:whopcom:exp-proxy",
|
|
29
|
+
verify_iss: true,
|
|
30
|
+
algorithm: "ES256"
|
|
31
|
+
})
|
|
32
|
+
payload
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
|
data/lib/whop/version.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require "rails/engine"
|
|
2
|
+
|
|
3
|
+
module Whop
|
|
4
|
+
module Webhooks
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
isolate_namespace Whop::Webhooks
|
|
7
|
+
|
|
8
|
+
initializer "whop.webhooks.routes" do
|
|
9
|
+
Whop::Webhooks::Engine.routes.draw do
|
|
10
|
+
post "/", to: "webhooks#receive"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "openssl"
|
|
2
|
+
|
|
3
|
+
module Whop
|
|
4
|
+
module Webhooks
|
|
5
|
+
module Signature
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Compute hex HMAC-SHA256 digest of the given payload using the secret.
|
|
9
|
+
def compute(secret, payload)
|
|
10
|
+
OpenSSL::HMAC.hexdigest("SHA256", secret, payload.to_s)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Compare provided signature header to computed digest in constant time.
|
|
14
|
+
# Accepts formats like "sha256=<hex>" or raw hex.
|
|
15
|
+
def valid?(secret, payload, provided)
|
|
16
|
+
return false if secret.to_s.empty? || payload.nil? || provided.to_s.empty?
|
|
17
|
+
given = provided.to_s
|
|
18
|
+
given = given.split("=", 2).last if given.include?("=")
|
|
19
|
+
expected_primary = compute(secret, payload)
|
|
20
|
+
return true if secure_compare(expected_primary, given)
|
|
21
|
+
# Fallback: tolerate JSON formatting differences (dev convenience)
|
|
22
|
+
normalized = normalize_json(payload)
|
|
23
|
+
if normalized
|
|
24
|
+
expected_canonical = compute(secret, normalized)
|
|
25
|
+
return true if secure_compare(expected_canonical, given)
|
|
26
|
+
end
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def normalize_json(payload)
|
|
31
|
+
begin
|
|
32
|
+
obj = JSON.parse(payload)
|
|
33
|
+
JSON.generate(obj)
|
|
34
|
+
rescue StandardError
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Constant-time comparison to avoid timing attacks.
|
|
40
|
+
def secure_compare(a, b)
|
|
41
|
+
return false unless a.bytesize == b.bytesize
|
|
42
|
+
l = a.unpack("C*")
|
|
43
|
+
r = b.unpack("C*")
|
|
44
|
+
result = 0
|
|
45
|
+
l.zip(r) { |x, y| result |= x ^ y }
|
|
46
|
+
result.zero?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
|
data/lib/whop.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "active_support"
|
|
2
|
+
require "active_support/core_ext/module/attribute_accessors"
|
|
3
|
+
|
|
4
|
+
module Whop
|
|
5
|
+
# Base error type for gem
|
|
6
|
+
require_relative "whop/error"
|
|
7
|
+
class Configuration
|
|
8
|
+
attr_accessor :app_id, :api_key, :webhook_secret, :agent_user_id, :company_id, :api_base_url
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@api_base_url = "https://api.whop.com"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
mattr_accessor :_config, instance_writer: false, default: Configuration.new
|
|
16
|
+
|
|
17
|
+
def self.configure
|
|
18
|
+
yield _config if block_given?
|
|
19
|
+
_config
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.config
|
|
23
|
+
_config
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.client
|
|
27
|
+
require_relative "whop/client"
|
|
28
|
+
@_client ||= Whop::Client.new(config)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.api
|
|
32
|
+
require_relative "whop/dsl"
|
|
33
|
+
DSL::ClientProxy.new(client, DSL.registry)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if defined?(Rails)
|
|
38
|
+
require_relative "whop/webhooks/engine"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Load default DSL resource mappings
|
|
42
|
+
require_relative "whop/dsl_prelude"
|
|
43
|
+
|
|
44
|
+
# Ensure webhook signature verifier is loaded for controller usage
|
|
45
|
+
require_relative "whop/webhooks/signature"
|
|
46
|
+
|
|
47
|
+
# Load controller helpers so apps can include Whop::ControllerHelpers
|
|
48
|
+
require_relative "whop/token"
|
|
49
|
+
require_relative "whop/controller_helpers"
|
|
50
|
+
|
|
51
|
+
|
metadata
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: whop
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nikhil Nelson
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-10-17 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.9'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.9'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: faraday-retry
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.2'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.2'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: activesupport
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '7.0'
|
|
48
|
+
- - "<"
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '9.0'
|
|
51
|
+
type: :runtime
|
|
52
|
+
prerelease: false
|
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '7.0'
|
|
58
|
+
- - "<"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '9.0'
|
|
61
|
+
- !ruby/object:Gem::Dependency
|
|
62
|
+
name: railties
|
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '7.0'
|
|
68
|
+
- - "<"
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '9.0'
|
|
71
|
+
type: :runtime
|
|
72
|
+
prerelease: false
|
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '7.0'
|
|
78
|
+
- - "<"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '9.0'
|
|
81
|
+
- !ruby/object:Gem::Dependency
|
|
82
|
+
name: rack
|
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '2.2'
|
|
88
|
+
- - "<"
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '4.0'
|
|
91
|
+
type: :runtime
|
|
92
|
+
prerelease: false
|
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '2.2'
|
|
98
|
+
- - "<"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '4.0'
|
|
101
|
+
- !ruby/object:Gem::Dependency
|
|
102
|
+
name: json
|
|
103
|
+
requirement: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - "~>"
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '2.6'
|
|
108
|
+
type: :runtime
|
|
109
|
+
prerelease: false
|
|
110
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - "~>"
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '2.6'
|
|
115
|
+
- !ruby/object:Gem::Dependency
|
|
116
|
+
name: jwt
|
|
117
|
+
requirement: !ruby/object:Gem::Requirement
|
|
118
|
+
requirements:
|
|
119
|
+
- - "~>"
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: '2.8'
|
|
122
|
+
type: :runtime
|
|
123
|
+
prerelease: false
|
|
124
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
125
|
+
requirements:
|
|
126
|
+
- - "~>"
|
|
127
|
+
- !ruby/object:Gem::Version
|
|
128
|
+
version: '2.8'
|
|
129
|
+
- !ruby/object:Gem::Dependency
|
|
130
|
+
name: rspec
|
|
131
|
+
requirement: !ruby/object:Gem::Requirement
|
|
132
|
+
requirements:
|
|
133
|
+
- - "~>"
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
version: '3.12'
|
|
136
|
+
type: :development
|
|
137
|
+
prerelease: false
|
|
138
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
139
|
+
requirements:
|
|
140
|
+
- - "~>"
|
|
141
|
+
- !ruby/object:Gem::Version
|
|
142
|
+
version: '3.12'
|
|
143
|
+
description: 'A Rails 7+ gem to build embedded Whop apps. Mirrors Whop''s Next.js
|
|
144
|
+
template: verification, access control, webhooks, and API client with a small meta-programming
|
|
145
|
+
DSL.'
|
|
146
|
+
email:
|
|
147
|
+
- thesolohacker47@gmail.com
|
|
148
|
+
executables: []
|
|
149
|
+
extensions: []
|
|
150
|
+
extra_rdoc_files: []
|
|
151
|
+
files:
|
|
152
|
+
- README.md
|
|
153
|
+
- config/routes.rb
|
|
154
|
+
- examples/rails_app/template.rb
|
|
155
|
+
- lib/generators/whop/discover_page/discover_page_generator.rb
|
|
156
|
+
- lib/generators/whop/discover_page/templates/discover_controller.rb
|
|
157
|
+
- lib/generators/whop/discover_page/templates/show.html.erb
|
|
158
|
+
- lib/generators/whop/install/install_generator.rb
|
|
159
|
+
- lib/generators/whop/install/templates/whop.rb
|
|
160
|
+
- lib/generators/whop/install/templates/whop_iframe.rb
|
|
161
|
+
- lib/generators/whop/scaffold/all/all_generator.rb
|
|
162
|
+
- lib/generators/whop/scaffold/company/company_generator.rb
|
|
163
|
+
- lib/generators/whop/scaffold/company/templates/companies_controller.rb
|
|
164
|
+
- lib/generators/whop/scaffold/company/templates/show.html.erb
|
|
165
|
+
- lib/generators/whop/scaffold/experience/experience_generator.rb
|
|
166
|
+
- lib/generators/whop/scaffold/experience/templates/experiences_controller.rb
|
|
167
|
+
- lib/generators/whop/scaffold/experience/templates/show.html.erb
|
|
168
|
+
- lib/generators/whop/webhooks/handler/handler_generator.rb
|
|
169
|
+
- lib/generators/whop/webhooks/handler/templates/job.rb
|
|
170
|
+
- lib/generators/whop/webhooks/install/install_generator.rb
|
|
171
|
+
- lib/whop.rb
|
|
172
|
+
- lib/whop/access.rb
|
|
173
|
+
- lib/whop/client.rb
|
|
174
|
+
- lib/whop/controller_helpers.rb
|
|
175
|
+
- lib/whop/dsl.rb
|
|
176
|
+
- lib/whop/dsl_prelude.rb
|
|
177
|
+
- lib/whop/error.rb
|
|
178
|
+
- lib/whop/token.rb
|
|
179
|
+
- lib/whop/version.rb
|
|
180
|
+
- lib/whop/webhooks/engine.rb
|
|
181
|
+
- lib/whop/webhooks/signature.rb
|
|
182
|
+
homepage: https://github.com/TheSoloHacker47/whop-gem
|
|
183
|
+
licenses:
|
|
184
|
+
- MIT
|
|
185
|
+
metadata: {}
|
|
186
|
+
post_install_message:
|
|
187
|
+
rdoc_options: []
|
|
188
|
+
require_paths:
|
|
189
|
+
- lib
|
|
190
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
191
|
+
requirements:
|
|
192
|
+
- - ">="
|
|
193
|
+
- !ruby/object:Gem::Version
|
|
194
|
+
version: '3.2'
|
|
195
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
196
|
+
requirements:
|
|
197
|
+
- - ">="
|
|
198
|
+
- !ruby/object:Gem::Version
|
|
199
|
+
version: '0'
|
|
200
|
+
requirements: []
|
|
201
|
+
rubygems_version: 3.5.3
|
|
202
|
+
signing_key:
|
|
203
|
+
specification_version: 4
|
|
204
|
+
summary: 'Rails integration for Whop Apps: config, token verification, access checks,
|
|
205
|
+
webhooks, generators.'
|
|
206
|
+
test_files: []
|