ruby_native 0.5.0 → 0.5.3
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/app/controllers/ruby_native/iap/completions_controller.rb +67 -0
- data/app/controllers/ruby_native/iap/purchases_controller.rb +18 -0
- data/app/controllers/ruby_native/iap/restores_controller.rb +43 -0
- data/app/controllers/ruby_native/webhooks/apple_controller.rb +30 -0
- data/app/models/ruby_native/iap/purchase_intent.rb +20 -0
- data/config/routes.rb +8 -0
- data/lib/generators/ruby_native/iap_generator.rb +38 -0
- data/lib/generators/ruby_native/templates/create_ruby_native_purchase_intents.rb +17 -0
- data/lib/ruby_native/certs/AppleRootCA-G3.cer +0 -0
- data/lib/ruby_native/engine.rb +10 -0
- data/lib/ruby_native/iap/apple_webhook_processor.rb +41 -0
- data/lib/ruby_native/iap/decodable.rb +55 -0
- data/lib/ruby_native/iap/event.rb +42 -0
- data/lib/ruby_native/iap/normalizable.rb +41 -0
- data/lib/ruby_native/iap/verifiable.rb +37 -0
- data/lib/ruby_native/version.rb +1 -1
- data/lib/ruby_native.rb +14 -0
- metadata +28 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d81a713c1634d9c87271bd6411bcc9b4056d872c80c41e202815e6e71e5d05b1
|
|
4
|
+
data.tar.gz: 758f9b00e03c0f1b4193a90ea81f16bf41dfab67c4a3998e381cc504e0a7278f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fed068320e56b295729ee0faab82405f37c2e23ba71475eebf0db6202ac415f030f050b8d63a154daabb25a0bf35574839c28a90738af74321fdc13596223f05
|
|
7
|
+
data.tar.gz: f2172a6767322cd9a2b61be6001db63ad7e2ca7aea7e640a70e8b2e27036d2a704dcf75b3e980305a7c7e3c45c38b24e978fcc8d7463bf46d252f80c4cf414a8
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module RubyNative
|
|
2
|
+
module IAP
|
|
3
|
+
class CompletionsController < ::ActionController::Base
|
|
4
|
+
include Verifiable
|
|
5
|
+
include Decodable
|
|
6
|
+
|
|
7
|
+
skip_forgery_protection
|
|
8
|
+
|
|
9
|
+
def create
|
|
10
|
+
intent = PurchaseIntent.find_by!(uuid: params[:uuid])
|
|
11
|
+
return head :ok if intent.completed?
|
|
12
|
+
|
|
13
|
+
transaction = verify_transaction!(intent)
|
|
14
|
+
|
|
15
|
+
intent.update!(status: :completed, environment: transaction["environment"]&.downcase)
|
|
16
|
+
|
|
17
|
+
event = Event.new(
|
|
18
|
+
type: "subscription.created",
|
|
19
|
+
status: "active",
|
|
20
|
+
owner_token: intent.customer_id,
|
|
21
|
+
product_id: transaction["productId"],
|
|
22
|
+
original_transaction_id: transaction["originalTransactionId"],
|
|
23
|
+
transaction_id: transaction["transactionId"],
|
|
24
|
+
purchase_date: parse_timestamp(transaction["purchaseDate"]),
|
|
25
|
+
expires_date: parse_timestamp(transaction["expiresDate"]),
|
|
26
|
+
environment: transaction["environment"]&.downcase,
|
|
27
|
+
notification_uuid: SecureRandom.uuid,
|
|
28
|
+
success_path: intent.success_path
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
RubyNative.fire_subscription_callbacks(event)
|
|
32
|
+
head :ok
|
|
33
|
+
rescue ActiveRecord::RecordNotFound
|
|
34
|
+
head :not_found
|
|
35
|
+
rescue VerificationError, JWT::DecodeError => e
|
|
36
|
+
Rails.logger.warn "[RubyNative] Completion verification failed: #{e.message}"
|
|
37
|
+
head :unprocessable_entity
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def verify_transaction!(intent)
|
|
43
|
+
signed_transaction = params[:signed_transaction]
|
|
44
|
+
|
|
45
|
+
if signed_transaction.present?
|
|
46
|
+
transaction = decode_and_verify_jws(signed_transaction)
|
|
47
|
+
if transaction["appAccountToken"] != intent.uuid
|
|
48
|
+
raise VerificationError, "appAccountToken does not match intent UUID"
|
|
49
|
+
end
|
|
50
|
+
transaction
|
|
51
|
+
elsif Rails.env.local?
|
|
52
|
+
# Allow unsigned completions for Xcode StoreKit testing in development.
|
|
53
|
+
{
|
|
54
|
+
"productId" => intent.product_id,
|
|
55
|
+
"originalTransactionId" => "xcode_#{intent.uuid}",
|
|
56
|
+
"transactionId" => "xcode_#{intent.uuid}",
|
|
57
|
+
"purchaseDate" => (Time.current.to_f * 1000).to_i,
|
|
58
|
+
"expiresDate" => (1.year.from_now.to_f * 1000).to_i,
|
|
59
|
+
"environment" => "Xcode"
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
raise VerificationError, "Missing signed_transaction"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module RubyNative
|
|
2
|
+
module IAP
|
|
3
|
+
class PurchasesController < ::ActionController::Base
|
|
4
|
+
skip_forgery_protection
|
|
5
|
+
|
|
6
|
+
def create
|
|
7
|
+
intent = PurchaseIntent.create!(
|
|
8
|
+
customer_id: params[:customer_id],
|
|
9
|
+
product_id: params[:product_id],
|
|
10
|
+
success_path: params[:success_path],
|
|
11
|
+
environment: params[:environment] || "production"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
render json: {uuid: intent.uuid, product_id: intent.product_id}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module RubyNative
|
|
2
|
+
module IAP
|
|
3
|
+
class RestoresController < ::ActionController::Base
|
|
4
|
+
include Verifiable
|
|
5
|
+
include Decodable
|
|
6
|
+
|
|
7
|
+
skip_forgery_protection
|
|
8
|
+
|
|
9
|
+
def create
|
|
10
|
+
customer_id = params[:customer_id]
|
|
11
|
+
return head :bad_request if customer_id.blank?
|
|
12
|
+
|
|
13
|
+
transactions = Array(params[:signed_transactions])
|
|
14
|
+
return head :bad_request if transactions.empty?
|
|
15
|
+
|
|
16
|
+
transactions.each do |signed_transaction|
|
|
17
|
+
transaction = decode_and_verify_jws(signed_transaction)
|
|
18
|
+
|
|
19
|
+
event = Event.new(
|
|
20
|
+
type: "subscription.created",
|
|
21
|
+
status: "active",
|
|
22
|
+
owner_token: customer_id,
|
|
23
|
+
product_id: transaction["productId"],
|
|
24
|
+
original_transaction_id: transaction["originalTransactionId"],
|
|
25
|
+
transaction_id: transaction["transactionId"],
|
|
26
|
+
purchase_date: parse_timestamp(transaction["purchaseDate"]),
|
|
27
|
+
expires_date: parse_timestamp(transaction["expiresDate"]),
|
|
28
|
+
environment: transaction["environment"]&.downcase,
|
|
29
|
+
notification_uuid: SecureRandom.uuid,
|
|
30
|
+
success_path: params[:success_path]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
RubyNative.fire_subscription_callbacks(event)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
head :ok
|
|
37
|
+
rescue VerificationError, JWT::DecodeError => e
|
|
38
|
+
Rails.logger.warn "[RubyNative] Restore verification failed: #{e.message}"
|
|
39
|
+
head :unprocessable_entity
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module RubyNative
|
|
2
|
+
module Webhooks
|
|
3
|
+
class AppleController < ::ActionController::Base
|
|
4
|
+
skip_forgery_protection
|
|
5
|
+
|
|
6
|
+
def create
|
|
7
|
+
payload = JSON.parse(request.raw_post)
|
|
8
|
+
signed_payload = payload["signedPayload"]
|
|
9
|
+
return head :ok unless signed_payload
|
|
10
|
+
|
|
11
|
+
# TEST notifications from Apple have no transaction data to process.
|
|
12
|
+
decoded = JWT.decode(signed_payload, nil, false).first
|
|
13
|
+
return head :ok if decoded["notificationType"] == "TEST"
|
|
14
|
+
|
|
15
|
+
processor = RubyNative::IAP::AppleWebhookProcessor.new
|
|
16
|
+
processor.process(signed_payload)
|
|
17
|
+
|
|
18
|
+
head :ok
|
|
19
|
+
rescue JSON::ParserError
|
|
20
|
+
head :bad_request
|
|
21
|
+
rescue RubyNative::IAP::VerificationError, JWT::DecodeError => e
|
|
22
|
+
Rails.logger.error "[RubyNative] Apple webhook verification failed: #{e.message}"
|
|
23
|
+
head :unprocessable_entity
|
|
24
|
+
rescue => e
|
|
25
|
+
Rails.logger.error "[RubyNative] Apple webhook error: #{e.message}"
|
|
26
|
+
head :internal_server_error
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RubyNative
|
|
2
|
+
module IAP
|
|
3
|
+
class PurchaseIntent < ::ActiveRecord::Base
|
|
4
|
+
self.table_name = "ruby_native_purchase_intents"
|
|
5
|
+
|
|
6
|
+
before_create :generate_uuid
|
|
7
|
+
|
|
8
|
+
enum :status, {pending: "pending", completed: "completed"}
|
|
9
|
+
enum :environment, {sandbox: "sandbox", production: "production", xcode: "xcode"}
|
|
10
|
+
|
|
11
|
+
validates :customer_id, presence: true
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def generate_uuid
|
|
16
|
+
self.uuid ||= SecureRandom.uuid
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/config/routes.rb
CHANGED
|
@@ -7,4 +7,12 @@ RubyNative::Engine.routes.draw do
|
|
|
7
7
|
get "start/:provider", to: "start#show", as: :start
|
|
8
8
|
resource :session, only: :show
|
|
9
9
|
end
|
|
10
|
+
namespace :webhooks do
|
|
11
|
+
resource :apple, only: :create, controller: "apple"
|
|
12
|
+
end
|
|
13
|
+
namespace :iap do
|
|
14
|
+
resources :purchases, only: :create
|
|
15
|
+
post "completions/:uuid", to: "completions#create", as: :completion
|
|
16
|
+
resource :restore, only: :create
|
|
17
|
+
end
|
|
10
18
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/migration"
|
|
3
|
+
|
|
4
|
+
module RubyNative
|
|
5
|
+
module Generators
|
|
6
|
+
class IapGenerator < Rails::Generators::Base
|
|
7
|
+
include Rails::Generators::Migration
|
|
8
|
+
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
def self.next_migration_number(dirname)
|
|
12
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def copy_migration
|
|
16
|
+
migration_template "create_ruby_native_purchase_intents.rb",
|
|
17
|
+
"db/migrate/create_ruby_native_purchase_intents.rb"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def print_next_steps
|
|
21
|
+
say ""
|
|
22
|
+
say "Ruby Native IAP installed!", :green
|
|
23
|
+
say ""
|
|
24
|
+
say " 1. Run migrations: bin/rails db:migrate"
|
|
25
|
+
say " 2. Add your callback in config/initializers/ruby_native.rb:"
|
|
26
|
+
say ""
|
|
27
|
+
say ' RubyNative.on_subscription_change do |event|'
|
|
28
|
+
say ' user = User.find_by(id: event.owner_token)'
|
|
29
|
+
say ' user&.update!(subscribed: event.active?)'
|
|
30
|
+
say ' end'
|
|
31
|
+
say ""
|
|
32
|
+
say " 3. Set your App Store Server Notification URL to:"
|
|
33
|
+
say " https://yourapp.com/native/webhooks/apple"
|
|
34
|
+
say ""
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CreateRubyNativePurchaseIntents < ActiveRecord::Migration[7.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :ruby_native_purchase_intents do |t|
|
|
4
|
+
t.string :uuid, null: false
|
|
5
|
+
t.string :customer_id, null: false
|
|
6
|
+
t.string :product_id
|
|
7
|
+
t.string :success_path
|
|
8
|
+
t.string :status, null: false, default: "pending"
|
|
9
|
+
t.string :environment
|
|
10
|
+
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :ruby_native_purchase_intents, :uuid, unique: true
|
|
15
|
+
add_index :ruby_native_purchase_intents, :customer_id
|
|
16
|
+
end
|
|
17
|
+
end
|
|
Binary file
|
data/lib/ruby_native/engine.rb
CHANGED
|
@@ -2,6 +2,16 @@ module RubyNative
|
|
|
2
2
|
class Engine < ::Rails::Engine
|
|
3
3
|
isolate_namespace RubyNative
|
|
4
4
|
|
|
5
|
+
initializer "ruby_native.inflections", before: "ruby_native.helpers" do
|
|
6
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
7
|
+
inflect.acronym "IAP"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
initializer "ruby_native.filter_params" do |app|
|
|
12
|
+
app.config.filter_parameters += [:signedPayload]
|
|
13
|
+
end
|
|
14
|
+
|
|
5
15
|
initializer "ruby_native.helpers" do
|
|
6
16
|
ActiveSupport.on_load(:action_controller_base) do
|
|
7
17
|
include RubyNative::NativeDetection
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module RubyNative
|
|
2
|
+
module IAP
|
|
3
|
+
class AppleWebhookProcessor
|
|
4
|
+
include Verifiable
|
|
5
|
+
include Decodable
|
|
6
|
+
include Normalizable
|
|
7
|
+
|
|
8
|
+
def process(signed_payload)
|
|
9
|
+
notification = parse_notification(signed_payload)
|
|
10
|
+
intent = PurchaseIntent.find_by(uuid: notification.app_account_token)
|
|
11
|
+
|
|
12
|
+
event = build_event(notification, intent)
|
|
13
|
+
|
|
14
|
+
intent&.update!(status: :completed) if intent&.pending?
|
|
15
|
+
|
|
16
|
+
RubyNative.fire_subscription_callbacks(event)
|
|
17
|
+
event
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def build_event(notification, intent)
|
|
23
|
+
type = normalized_type(notification)
|
|
24
|
+
|
|
25
|
+
Event.new(
|
|
26
|
+
type: type,
|
|
27
|
+
status: STATUS_MAPPING[type] || "active",
|
|
28
|
+
owner_token: intent&.customer_id,
|
|
29
|
+
product_id: notification.product_id,
|
|
30
|
+
original_transaction_id: notification.original_transaction_id,
|
|
31
|
+
transaction_id: notification.transaction_id,
|
|
32
|
+
purchase_date: notification.purchase_date,
|
|
33
|
+
expires_date: notification.expires_date,
|
|
34
|
+
environment: notification.environment,
|
|
35
|
+
notification_uuid: notification.notification_uuid,
|
|
36
|
+
success_path: intent&.success_path
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "jwt"
|
|
2
|
+
|
|
3
|
+
module RubyNative
|
|
4
|
+
module IAP
|
|
5
|
+
module Decodable
|
|
6
|
+
Notification = Data.define(
|
|
7
|
+
:notification_type,
|
|
8
|
+
:subtype,
|
|
9
|
+
:notification_uuid,
|
|
10
|
+
:bundle_id,
|
|
11
|
+
:app_account_token,
|
|
12
|
+
:product_id,
|
|
13
|
+
:original_transaction_id,
|
|
14
|
+
:transaction_id,
|
|
15
|
+
:purchase_date,
|
|
16
|
+
:expires_date,
|
|
17
|
+
:offer_type,
|
|
18
|
+
:environment
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def parse_notification(signed_payload)
|
|
24
|
+
payload = decode_and_verify_jws(signed_payload)
|
|
25
|
+
transaction_info = decode_and_verify_jws(payload["data"]["signedTransactionInfo"])
|
|
26
|
+
|
|
27
|
+
Notification.new(
|
|
28
|
+
notification_type: payload["notificationType"],
|
|
29
|
+
subtype: payload["subtype"],
|
|
30
|
+
notification_uuid: payload["notificationUUID"],
|
|
31
|
+
bundle_id: payload["data"]["bundleId"],
|
|
32
|
+
app_account_token: transaction_info["appAccountToken"],
|
|
33
|
+
product_id: transaction_info["productId"],
|
|
34
|
+
original_transaction_id: transaction_info["originalTransactionId"],
|
|
35
|
+
transaction_id: transaction_info["transactionId"],
|
|
36
|
+
purchase_date: parse_timestamp(transaction_info["purchaseDate"]),
|
|
37
|
+
expires_date: parse_timestamp(transaction_info["expiresDate"]),
|
|
38
|
+
offer_type: transaction_info["offerType"],
|
|
39
|
+
environment: payload["data"]["environment"]&.downcase
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def decode_and_verify_jws(jws)
|
|
44
|
+
JWT.decode(jws, nil, true, algorithm: "ES256") { |header|
|
|
45
|
+
verify_certificate_chain(header["x5c"])
|
|
46
|
+
}.first
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_timestamp(milliseconds)
|
|
50
|
+
return nil if milliseconds.nil?
|
|
51
|
+
Time.at(milliseconds / 1000.0).utc
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module RubyNative
|
|
2
|
+
module IAP
|
|
3
|
+
class Event
|
|
4
|
+
attr_reader :type, :status, :owner_token, :product_id,
|
|
5
|
+
:original_transaction_id, :transaction_id,
|
|
6
|
+
:purchase_date, :expires_date, :environment,
|
|
7
|
+
:notification_uuid, :success_path
|
|
8
|
+
|
|
9
|
+
def initialize(type:, status:, owner_token:, product_id:, original_transaction_id:,
|
|
10
|
+
transaction_id:, purchase_date:, expires_date:, environment:,
|
|
11
|
+
notification_uuid:, success_path:)
|
|
12
|
+
@type = type
|
|
13
|
+
@status = status
|
|
14
|
+
@owner_token = owner_token
|
|
15
|
+
@product_id = product_id
|
|
16
|
+
@original_transaction_id = original_transaction_id
|
|
17
|
+
@transaction_id = transaction_id
|
|
18
|
+
@purchase_date = purchase_date
|
|
19
|
+
@expires_date = expires_date
|
|
20
|
+
@environment = environment
|
|
21
|
+
@notification_uuid = notification_uuid
|
|
22
|
+
@success_path = success_path
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def active?
|
|
26
|
+
status == "active"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def expired?
|
|
30
|
+
status == "expired"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def created?
|
|
34
|
+
type == "subscription.created"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def canceled?
|
|
38
|
+
type == "subscription.canceled"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module RubyNative
|
|
2
|
+
module IAP
|
|
3
|
+
module Normalizable
|
|
4
|
+
TYPE_MAPPING = {
|
|
5
|
+
"SUBSCRIBED" => "subscription.created",
|
|
6
|
+
"DID_RENEW" => "subscription.updated",
|
|
7
|
+
"DID_CHANGE_RENEWAL_STATUS" => {
|
|
8
|
+
"AUTO_RENEW_DISABLED" => "subscription.canceled",
|
|
9
|
+
"AUTO_RENEW_ENABLED" => "subscription.updated"
|
|
10
|
+
},
|
|
11
|
+
"DID_CHANGE_RENEWAL_INFO" => {
|
|
12
|
+
"UPGRADE" => "subscription.updated",
|
|
13
|
+
"DOWNGRADE" => "subscription.updated"
|
|
14
|
+
},
|
|
15
|
+
"EXPIRED" => "subscription.expired",
|
|
16
|
+
"DID_FAIL_TO_RENEW" => "subscription.updated",
|
|
17
|
+
"GRACE_PERIOD_EXPIRED" => "subscription.expired",
|
|
18
|
+
"REFUND" => "subscription.expired"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
STATUS_MAPPING = {
|
|
22
|
+
"subscription.created" => "active",
|
|
23
|
+
"subscription.updated" => "active",
|
|
24
|
+
"subscription.canceled" => "active",
|
|
25
|
+
"subscription.expired" => "expired"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def normalized_type(notification)
|
|
31
|
+
mapping = TYPE_MAPPING[notification.notification_type]
|
|
32
|
+
|
|
33
|
+
if mapping.is_a?(Hash)
|
|
34
|
+
mapping[notification.subtype] || "subscription.updated"
|
|
35
|
+
else
|
|
36
|
+
mapping || "subscription.updated"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module RubyNative
|
|
2
|
+
module IAP
|
|
3
|
+
class VerificationError < StandardError; end
|
|
4
|
+
|
|
5
|
+
module Verifiable
|
|
6
|
+
APPLE_ROOT_CERT_PATH = File.join(__dir__, "..", "certs", "AppleRootCA-G3.cer")
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def verify_certificate_chain(x5c_certs)
|
|
11
|
+
raise VerificationError, "Missing x5c certificates" if x5c_certs.nil? || x5c_certs.empty?
|
|
12
|
+
|
|
13
|
+
certs = x5c_certs.map do |cert_base64|
|
|
14
|
+
OpenSSL::X509::Certificate.new(Base64.decode64(cert_base64))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
unless certs.last.to_der == apple_root_certificate.to_der
|
|
18
|
+
raise VerificationError, "Root certificate does not match Apple's known root"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
certs.each_cons(2) do |child, parent|
|
|
22
|
+
unless child.verify(parent.public_key)
|
|
23
|
+
raise VerificationError, "Certificate chain verification failed"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
certs.first.public_key
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def apple_root_certificate
|
|
31
|
+
@apple_root_certificate ||= OpenSSL::X509::Certificate.new(
|
|
32
|
+
File.read(APPLE_ROOT_CERT_PATH)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/ruby_native/version.rb
CHANGED
data/lib/ruby_native.rb
CHANGED
|
@@ -5,10 +5,24 @@ require "ruby_native/native_detection"
|
|
|
5
5
|
require "ruby_native/inertia_support"
|
|
6
6
|
require "ruby_native/oauth_middleware"
|
|
7
7
|
require "ruby_native/tunnel_cookie_middleware"
|
|
8
|
+
require "ruby_native/iap/event"
|
|
9
|
+
require "ruby_native/iap/verifiable"
|
|
10
|
+
require "ruby_native/iap/decodable"
|
|
11
|
+
require "ruby_native/iap/normalizable"
|
|
12
|
+
require "ruby_native/iap/apple_webhook_processor"
|
|
8
13
|
require "ruby_native/engine"
|
|
9
14
|
|
|
10
15
|
module RubyNative
|
|
11
16
|
mattr_accessor :config
|
|
17
|
+
mattr_accessor :subscription_callbacks, default: []
|
|
18
|
+
|
|
19
|
+
def self.on_subscription_change(&block)
|
|
20
|
+
subscription_callbacks << block
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.fire_subscription_callbacks(event)
|
|
24
|
+
subscription_callbacks.each { |cb| cb.call(event) }
|
|
25
|
+
end
|
|
12
26
|
|
|
13
27
|
def self.load_config
|
|
14
28
|
path = Rails.root.join("config", "ruby_native.yml")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_native
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joe Masilotti
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '3.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: jwt
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
40
54
|
description: Turn your existing Rails app into a native iOS and Android app with Ruby
|
|
41
55
|
Native.
|
|
42
56
|
email:
|
|
@@ -52,7 +66,11 @@ files:
|
|
|
52
66
|
- app/controllers/ruby_native/auth/sessions_controller.rb
|
|
53
67
|
- app/controllers/ruby_native/auth/start_controller.rb
|
|
54
68
|
- app/controllers/ruby_native/config_controller.rb
|
|
69
|
+
- app/controllers/ruby_native/iap/completions_controller.rb
|
|
70
|
+
- app/controllers/ruby_native/iap/purchases_controller.rb
|
|
71
|
+
- app/controllers/ruby_native/iap/restores_controller.rb
|
|
55
72
|
- app/controllers/ruby_native/push/devices_controller.rb
|
|
73
|
+
- app/controllers/ruby_native/webhooks/apple_controller.rb
|
|
56
74
|
- app/javascript/ruby_native/back.js
|
|
57
75
|
- app/javascript/ruby_native/bridge/badge_controller.js
|
|
58
76
|
- app/javascript/ruby_native/bridge/button_controller.js
|
|
@@ -65,14 +83,18 @@ files:
|
|
|
65
83
|
- app/javascript/ruby_native/bridge/tabs_controller.js
|
|
66
84
|
- app/javascript/ruby_native/react.js
|
|
67
85
|
- app/javascript/ruby_native/vue.js
|
|
86
|
+
- app/models/ruby_native/iap/purchase_intent.rb
|
|
68
87
|
- app/views/ruby_native/auth/start/show.html.erb
|
|
69
88
|
- config/importmap.rb
|
|
70
89
|
- config/routes.rb
|
|
71
90
|
- exe/ruby_native
|
|
91
|
+
- lib/generators/ruby_native/iap_generator.rb
|
|
72
92
|
- lib/generators/ruby_native/install_generator.rb
|
|
73
93
|
- lib/generators/ruby_native/templates/CLAUDE.md
|
|
94
|
+
- lib/generators/ruby_native/templates/create_ruby_native_purchase_intents.rb
|
|
74
95
|
- lib/generators/ruby_native/templates/ruby_native.yml
|
|
75
96
|
- lib/ruby_native.rb
|
|
97
|
+
- lib/ruby_native/certs/AppleRootCA-G3.cer
|
|
76
98
|
- lib/ruby_native/cli.rb
|
|
77
99
|
- lib/ruby_native/cli/credentials.rb
|
|
78
100
|
- lib/ruby_native/cli/deploy.rb
|
|
@@ -81,6 +103,11 @@ files:
|
|
|
81
103
|
- lib/ruby_native/cli/screenshots.rb
|
|
82
104
|
- lib/ruby_native/engine.rb
|
|
83
105
|
- lib/ruby_native/helper.rb
|
|
106
|
+
- lib/ruby_native/iap/apple_webhook_processor.rb
|
|
107
|
+
- lib/ruby_native/iap/decodable.rb
|
|
108
|
+
- lib/ruby_native/iap/event.rb
|
|
109
|
+
- lib/ruby_native/iap/normalizable.rb
|
|
110
|
+
- lib/ruby_native/iap/verifiable.rb
|
|
84
111
|
- lib/ruby_native/inertia_support.rb
|
|
85
112
|
- lib/ruby_native/native_detection.rb
|
|
86
113
|
- lib/ruby_native/native_version.rb
|