spree_razorpay_checkout 0.1.3 → 0.2.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.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/spree/products_controller_decorator.rb +1 -1
  3. data/app/controllers/spree/razorpay/webhooks_controller.rb +35 -0
  4. data/app/javascript/spree_razorpay/application.js +12 -0
  5. data/app/javascript/spree_razorpay/controllers/checkout_razorpay_controller.js +108 -0
  6. data/app/jobs/spree_razorpay_checkout/handle_webhook_event_job.rb +64 -0
  7. data/app/models/spree/gateway/razorpay_gateway.rb +96 -53
  8. data/app/models/spree/page_blocks/products/razorpay_affordability.rb +4 -4
  9. data/app/models/spree/page_sections/product_details_decorator.rb +0 -1
  10. data/app/models/spree/payment_sessions/razorpay.rb +43 -0
  11. data/app/models/spree/razorpay_checkout.rb +1 -0
  12. data/app/services/razorpay/rp_order/api.rb +9 -1
  13. data/app/views/spree/admin/payment_methods/configuration_guides/_razorpay.html.erb +1 -1
  14. data/app/views/spree/admin/payment_methods/descriptions/_razorpay.html.erb +1 -0
  15. data/app/views/spree/checkout/payment/_razorpay.html.erb +121 -186
  16. data/config/routes.rb +4 -4
  17. data/lib/generators/spree_razorpay_checkout/install/install_generator.rb +21 -3
  18. data/lib/spree_razorpay_checkout/engine.rb +11 -7
  19. data/lib/spree_razorpay_checkout/version.rb +1 -1
  20. metadata +13 -43
  21. data/README.md +0 -188
  22. data/app/assets/images/payment_icons/icon_razorpay.svg +0 -20
  23. data/app/assets/javascripts/spree/frontend/process_razorpay.js +0 -40
  24. data/app/assets/javascripts/spree/frontend/spree_razorpay.js +0 -33
  25. data/app/views/themes/default/spree/page_sections/_product_details.html.erb +0 -88
  26. data/config/application.rb +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fa91a572f8ba303d47dd90b0142eff75dc21478b85c9d85a9d2ca842318a4b9
4
- data.tar.gz: 889b39cf1a9076a0b88d6d9a08d06971d4890f9fb08dbd94b4e9ba170f2d92a2
3
+ metadata.gz: 210a8729bc5810c991d970ad880aa0a0ac8570e9eccfd0de194e13cb600d0136
4
+ data.tar.gz: 2198af0c4bdf320756d3ca5a197a53b7f34892332f649e284ad98892355aeadd
5
5
  SHA512:
6
- metadata.gz: f11e14acd2e4325710adc68337f10fd3d8ab3736dc5afb9d1dc91d91af5991ac3e9feaf296f337bad37ff5f469d2d4778c9dc33ecfac8314daf95766ebb9684e
7
- data.tar.gz: 21c4b3840513e8a19987a9901d5afe1d32f18832869e876c0bc03098165ab703a55a95e101a2092403458c4e1a5314de46d2474ea786c8b69e14a3fb190ceb91
6
+ metadata.gz: 5fc643f93e86598dbf6586e333a54af34ceb300b523c3a6168bedb2164ed1112adb53707c9ac22637b54f1a1c77c1f44edbd1a96a81cda28de9147fbe7aee4eb
7
+ data.tar.gz: 1df23f297c1a5629721d24d6db194a2b1ae2a8b6adf3b4eacd46ec658e8d4791637ec0d4282aff14876c9d1bf120621b238ad99f5cc618598f508e679cb4a497
@@ -14,4 +14,4 @@ module Spree
14
14
  end
15
15
  end
16
16
 
17
- ::Spree::ProductsController.prepend(Spree::ProductsControllerDecorator)
17
+ ::Spree::ProductsController.prepend(Spree::ProductsControllerDecorator)
@@ -0,0 +1,35 @@
1
+ module Spree
2
+ module Razorpay
3
+ class WebhooksController < ActionController::API
4
+ skip_before_action :verify_authenticity_token, raise: false
5
+
6
+ def create
7
+ payload = request.body.read
8
+ signature = request.headers['X-Razorpay-Signature']
9
+
10
+ gateway = Spree::Gateway::RazorpayGateway.active.first
11
+ return head :not_found unless gateway && gateway.preferred_webhook_secret.present?
12
+
13
+ begin
14
+ ::Razorpay::Utility.verify_webhook_signature(
15
+ payload,
16
+ signature,
17
+ gateway.preferred_webhook_secret
18
+ )
19
+ rescue ::Razorpay::Errors::SignatureVerificationError => e
20
+ Rails.logger.error("Razorpay Webhook Verification Failed: #{e.message}")
21
+ return head :unauthorized
22
+ end
23
+
24
+ event = JSON.parse(payload)
25
+
26
+ # Listen for 'payment.authorized' which fires immediately after OTP!
27
+ if ['order.paid', 'payment.captured', 'payment.authorized'].include?(event['event'])
28
+ ::SpreeRazorpayCheckout::HandleWebhookEventJob.perform_later(event)
29
+ end
30
+
31
+ head :ok
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ import { Application } from '@hotwired/stimulus'
2
+ import CheckoutRazorpayController from './controllers/checkout_razorpay_controller'
3
+
4
+ let application;
5
+ if (typeof window.Stimulus === "undefined") {
6
+ application = Application.start()
7
+ window.Stimulus = application
8
+ } else {
9
+ application = window.Stimulus
10
+ }
11
+
12
+ application.register('checkout-razorpay', CheckoutRazorpayController)
@@ -0,0 +1,108 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ paymentMethodId: String,
6
+ keyId: String,
7
+ orderId: String,
8
+ amount: Number,
9
+ currency: String,
10
+ merchantName: String,
11
+ merchantDesc: String,
12
+ themeColor: String,
13
+ userName: String,
14
+ userEmail: String,
15
+ userContact: String
16
+ }
17
+
18
+ static targets = [
19
+ 'paymentId',
20
+ 'orderId',
21
+ 'signature'
22
+ ]
23
+
24
+ connect() {
25
+ this.form = document.querySelector("#checkout_form_payment")
26
+ this.submitBtn = document.getElementById("checkout-payment-submit")
27
+
28
+ // Bind to the form submit event, NOT the button click!
29
+ // This perfectly intercepts Spree 5.3's native validation flows.
30
+ this.submitHandler = this.submit.bind(this)
31
+ this.form.addEventListener('submit', this.submitHandler)
32
+ }
33
+
34
+ disconnect() {
35
+ if (this.form) {
36
+ this.form.removeEventListener('submit', this.submitHandler)
37
+ }
38
+ }
39
+
40
+ submit(e) {
41
+ // Check if Razorpay is the currently selected radio button
42
+ const selectedRadio = document.querySelector('input[name="order[payments_attributes][][payment_method_id]"]:checked');
43
+ const isRazorpay = selectedRadio && selectedRadio.value === this.paymentMethodIdValue;
44
+
45
+ if (!isRazorpay) return; // If it's a different gateway, let Spree handle it normally
46
+
47
+ // If we already have the Razorpay signature populated, let the form submit natively to the backend!
48
+ if (this.paymentIdTarget.value && this.signatureTarget.value) {
49
+ return true;
50
+ }
51
+
52
+ // Otherwise, STOP the form submission and open Razorpay
53
+ e.preventDefault();
54
+ e.stopImmediatePropagation();
55
+
56
+ this.setLoading(true);
57
+
58
+ const options = {
59
+ key: this.keyIdValue,
60
+ order_id: this.orderIdValue,
61
+ amount: this.amountValue,
62
+ currency: this.currencyValue,
63
+ name: this.merchantNameValue,
64
+ description: this.merchantDescValue,
65
+ handler: this.handleSuccess.bind(this),
66
+ modal: {
67
+ ondismiss: this.handleDismiss.bind(this)
68
+ },
69
+ prefill: {
70
+ name: this.userNameValue,
71
+ email: this.userEmailValue,
72
+ contact: this.userContactValue
73
+ },
74
+ theme: {
75
+ color: this.themeColorValue
76
+ }
77
+ };
78
+
79
+ const rzp = new window.Razorpay(options);
80
+ rzp.open();
81
+ }
82
+
83
+ handleSuccess(response) {
84
+ // 1. Populate the hidden fields with the secure response
85
+ this.paymentIdTarget.value = response.razorpay_payment_id;
86
+ this.orderIdTarget.value = response.razorpay_order_id;
87
+ this.signatureTarget.value = response.razorpay_signature;
88
+
89
+ // 2. Submit the form natively to Spree's backend!
90
+ // requestSubmit() ensures Turbo and Spree's event listeners fire correctly
91
+ this.form.requestSubmit();
92
+ }
93
+
94
+ handleDismiss() {
95
+ this.setLoading(false);
96
+ }
97
+
98
+ setLoading(isLoading) {
99
+ if (isLoading) {
100
+ this.submitBtn.disabled = true;
101
+ this.submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Processing...';
102
+ } else {
103
+ this.submitBtn.disabled = false;
104
+ // Reset Spree button text depending on what it usually says
105
+ this.submitBtn.innerHTML = 'Save and Continue';
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,64 @@
1
+ module SpreeRazorpayCheckout
2
+ class HandleWebhookEventJob < ActiveJob::Base
3
+ queue_as :default
4
+
5
+ def perform(event)
6
+ payload = event['payload']
7
+ payment_entity = payload.dig('payment', 'entity') || payload.dig('order', 'entity')
8
+
9
+ # Razorpay might send the ID in slightly different places depending on the event
10
+ razorpay_order_id = payment_entity['order_id'] || payload.dig('order', 'entity', 'id')
11
+ razorpay_payment_id = payment_entity['id']
12
+
13
+ checkout_record = Spree::RazorpayCheckout.find_by(razorpay_order_id: razorpay_order_id)
14
+ return unless checkout_record
15
+
16
+ order = checkout_record.order
17
+ return if order.completed? || order.canceled?
18
+
19
+ gateway = Spree::Gateway::RazorpayGateway.active.first
20
+ return unless gateway
21
+
22
+ order.with_lock do
23
+ # 1. Capture the abandoned payment via API
24
+ begin
25
+ rzp_payment = ::Razorpay::Payment.fetch(razorpay_payment_id)
26
+ if rzp_payment.status == 'authorized'
27
+ amount_in_cents = (order.total_minus_store_credits.to_f * 100).to_i
28
+ rzp_payment.capture({ amount: amount_in_cents })
29
+ end
30
+ rescue StandardError => e
31
+ Rails.logger.error("Webhook Razorpay Capture Failed: #{e.message}")
32
+ end
33
+
34
+ checkout_record.update!(
35
+ razorpay_payment_id: razorpay_payment_id,
36
+ status: 'captured'
37
+ )
38
+
39
+ # 2. Create the Spree::Payment record natively
40
+ payment = order.payments.find_or_create_by!(
41
+ response_code: razorpay_payment_id,
42
+ payment_method_id: gateway.id
43
+ ) do |p|
44
+ p.amount = order.total
45
+ p.source = checkout_record
46
+ p.state = 'checkout'
47
+ end
48
+
49
+ # 3. Mark the payment as completed
50
+ payment.process! if payment.checkout?
51
+ payment.complete! if payment.pending? || payment.processing?
52
+
53
+ # Tell Spree to recalculate the payment state
54
+ order.updater.update_payment_state
55
+
56
+ # Loop through the checkout steps (Payment -> Confirm -> Complete)
57
+ until order.completed? || order.state == 'complete'
58
+ order.next!
59
+ end
60
+
61
+ end
62
+ end
63
+ end
64
+ end
@@ -2,10 +2,11 @@ require 'razorpay'
2
2
 
3
3
  module Spree
4
4
  class Gateway::RazorpayGateway < Gateway
5
+ preference :webhook_secret, :password
5
6
  preference :key_id, :string
6
- preference :key_secret, :string
7
+ preference :key_secret, :password
7
8
  preference :test_key_id, :string
8
- preference :test_key_secret, :string
9
+ preference :test_key_secret, :password
9
10
  preference :test_mode, :boolean, default: false
10
11
  preference :merchant_name, :string, default: 'Razorpay'
11
12
  preference :merchant_description, :text, default: 'Razorpay Payment Gateway'
@@ -17,7 +18,11 @@ module Spree
17
18
  end
18
19
 
19
20
  def source_required?
20
- false
21
+ true
22
+ end
23
+
24
+ def payment_source_class
25
+ Spree::RazorpayCheckout
21
26
  end
22
27
 
23
28
  def name
@@ -28,10 +33,6 @@ module Spree
28
33
  'razorpay'
29
34
  end
30
35
 
31
- def payment_source_class
32
- 'razorpay'
33
- end
34
-
35
36
  def payment_icon_name
36
37
  'razorpay'
37
38
  end
@@ -49,7 +50,7 @@ module Spree
49
50
  end
50
51
 
51
52
  def provider
52
- Razorpay.setup(current_key_id, current_key_secret)
53
+ ::Razorpay.setup(current_key_id, current_key_secret)
53
54
  end
54
55
 
55
56
  def current_key_id
@@ -69,7 +70,7 @@ module Spree
69
70
  end
70
71
 
71
72
  def actions
72
- %w[capture void]
73
+ %w[capture void credit]
73
74
  end
74
75
 
75
76
  def can_capture?(payment)
@@ -80,63 +81,105 @@ module Spree
80
81
  payment.state != 'void'
81
82
  end
82
83
 
83
- # Not used directly (we use custom flow), but kept it for compatibility
84
- def purchase(_amount, _transaction_details, _gateway_options = {})
85
- ActiveMerchant::Billing::Response.new(true, 'Razorpay success')
84
+ # -------------------------------------------------------------------------
85
+ # PURCHASE (Happy Path & Webhook Recovery)
86
+ # -------------------------------------------------------------------------
87
+ def purchase(_amount, source, _gateway_options = {})
88
+ provider
89
+
90
+ begin
91
+ if source.razorpay_payment_id.blank? || source.razorpay_signature.blank?
92
+ return ActiveMerchant::Billing::Response.new(false, 'Payment was not completed. Please try again.', {}, test: preferred_test_mode)
93
+ end
94
+
95
+ # 1. Verify the signature
96
+ ::Razorpay::Utility.verify_payment_signature(
97
+ razorpay_order_id: source.razorpay_order_id,
98
+ razorpay_payment_id: source.razorpay_payment_id,
99
+ razorpay_signature: source.razorpay_signature
100
+ )
101
+
102
+ # 2. Safely ensure it is captured!
103
+ rzp_payment = ::Razorpay::Payment.fetch(source.razorpay_payment_id)
104
+ if rzp_payment.status == 'authorized'
105
+ rzp_payment.capture({ amount: _amount })
106
+ end
107
+
108
+ source.update!(status: 'captured')
109
+
110
+ ActiveMerchant::Billing::Response.new(
111
+ true,
112
+ 'Razorpay Payment Successful',
113
+ {},
114
+ test: preferred_test_mode,
115
+ authorization: source.razorpay_payment_id
116
+ )
117
+
118
+ rescue StandardError => e
119
+ Rails.logger.error("Razorpay Verification/Capture Failed: #{e.message}")
120
+ ActiveMerchant::Billing::Response.new(false, 'Payment verification failed.', {}, test: preferred_test_mode)
121
+ end
86
122
  end
87
123
 
88
124
  def capture(*args)
89
- simulated_successful_billing_response
125
+ # We already auto-capture via the frontend/webhook, so we return true to keep Spree happy
126
+ ActiveMerchant::Billing::Response.new(true, 'Already Captured', {}, test: preferred_test_mode)
90
127
  end
91
128
 
92
- def void(*)
93
- simulated_successful_billing_response
94
- end
129
+ # -------------------------------------------------------------------------
130
+ # REFUNDS & CANCELLATIONS
131
+ # -------------------------------------------------------------------------
95
132
 
96
- def credit(_credit_cents, _payment_id, _options)
97
- ActiveMerchant::Billing::Response.new(true, 'Refund successful')
98
- end
133
+ # Triggered when you click "Refund" in the Spree Admin
134
+ def credit(credit_cents, response_code, _gateway_options = {})
135
+ provider
99
136
 
100
- def cancel(payment, _options = {})
101
- # If `payment` is a Spree::Payment, use its source
102
- source = if payment.respond_to?(:source)
103
- payment.source else payment end
104
- payment.void! if payment.respond_to?(:void!)
105
- if source.respond_to?(:razorpay_payment_id)
106
- # Uncomment if you want to actually trigger refund
107
- # Razorpay::Payment.fetch(source.razorpay_payment_id).refund
108
- OpenStruct.new(success?: true, authorization: source.razorpay_payment_id)
109
- else
110
- # fallback for string/unknown source
111
- OpenStruct.new(success?: true, authorization: nil)
112
- end
113
- rescue => e
114
- Rails.logger.error("Razorpay cancel failed: #{e.message}")
115
- OpenStruct.new(success?: false, message: e.message)
137
+ begin
138
+ # Fetch the original payment from Razorpay using the saved payment ID
139
+ rzp_payment = ::Razorpay::Payment.fetch(response_code)
140
+
141
+ # Issue the refund via Razorpay API (amount must be in paise/cents)
142
+ refund = rzp_payment.refund(amount: credit_cents)
143
+
144
+ ActiveMerchant::Billing::Response.new(
145
+ true,
146
+ 'Razorpay Refund Successful',
147
+ { refund_id: refund.id },
148
+ test: preferred_test_mode,
149
+ authorization: refund.id
150
+ )
151
+ rescue StandardError => e
152
+ Rails.logger.error("Razorpay Refund Failed: #{e.message}")
153
+ ActiveMerchant::Billing::Response.new(false, "Refund failed: #{e.message}", {}, test: preferred_test_mode)
154
+ end
116
155
  end
117
156
 
118
- # Verify signature, fetch payment and capture if required. Returns Razorpay::Payment object.
119
- def verify_and_capture_razorpay_payment(order, razorpay_payment_id)
120
- Razorpay.setup(current_key_id, current_key_secret)
157
+ # Triggered if you explicitly "Void" a payment in Spree
158
+ def void(response_code, _gateway_options = {})
159
+ provider
121
160
 
122
161
  begin
123
- payment = Razorpay::Payment.fetch(razorpay_payment_id)
124
- # If payment is not captured and auto_capture set true, capture it
125
- if payment.status == 'authorized'
126
- amount = order.inr_amt_in_paise
127
- payment = payment.capture(amount: amount)
128
- end
129
-
130
- payment
131
- rescue Razorpay::Error => e
132
- raise Spree::Core::GatewayError, "Razorpay error: #{e.message}"
162
+ # Razorpay doesn't have a concept of "Voiding" a captured payment,
163
+ # so we just issue a full refund instead.
164
+ rzp_payment = ::Razorpay::Payment.fetch(response_code)
165
+ refund = rzp_payment.refund
166
+
167
+ ActiveMerchant::Billing::Response.new(
168
+ true,
169
+ 'Razorpay Void/Refund Successful',
170
+ { refund_id: refund.id },
171
+ test: preferred_test_mode,
172
+ authorization: refund.id
173
+ )
174
+ rescue StandardError => e
175
+ Rails.logger.error("Razorpay Void Failed: #{e.message}")
176
+ ActiveMerchant::Billing::Response.new(false, "Void failed: #{e.message}", {}, test: preferred_test_mode)
133
177
  end
134
178
  end
135
179
 
136
- private
137
-
138
- def simulated_successful_billing_response
139
- ActiveMerchant::Billing::Response.new(true, '', {}, {})
180
+ # Triggered if the entire Order is Cancelled in the Spree Admin
181
+ def cancel(response_code)
182
+ void(response_code)
140
183
  end
141
184
  end
142
- end
185
+ end
@@ -55,7 +55,7 @@ module Spree
55
55
  is_available = available?(locals)
56
56
  Rails.logger.info " Available check: #{is_available}"
57
57
  unless is_available
58
- Rails.logger.warn " ⚠️ Block marked as not available, but rendering anyway"
58
+ Rails.logger.warn " Block marked as not available, but rendering anyway"
59
59
  end
60
60
  end
61
61
 
@@ -63,13 +63,13 @@ module Spree
63
63
  Rails.logger.info " Rendering partial: spree/page_blocks/products/razorpay_affordability/razorpay_affordability"
64
64
  result = view_context.render partial: 'spree/page_blocks/products/razorpay_affordability/razorpay_affordability',
65
65
  locals: locals.merge(block: self, page_block: self)
66
- Rails.logger.info " Render successful, output length: #{result.to_s.length}"
66
+ Rails.logger.info " Render successful, output length: #{result.to_s.length}"
67
67
  result
68
68
  rescue ActionView::MissingTemplate => e
69
- Rails.logger.error " Missing template: #{e.message}"
69
+ Rails.logger.error " Missing template: #{e.message}"
70
70
  ''
71
71
  rescue => e
72
- Rails.logger.error " Error rendering Razorpay Affordability block: #{e.message}"
72
+ Rails.logger.error " Error rendering Razorpay Affordability block: #{e.message}"
73
73
  "<div class='razorpay-affordability-error'>Error loading Razorpay Affordability Widget</div>".html_safe
74
74
  end
75
75
  end
@@ -15,4 +15,3 @@ module Spree
15
15
  Spree::PageSections::ProductDetails.prepend(
16
16
  Spree::PageSections::ProductDetailsDecorator
17
17
  )
18
-
@@ -0,0 +1,43 @@
1
+ module Spree
2
+ module PaymentSessions
3
+ class Razorpay < PaymentSession
4
+ # external_id will store the razorpay_order_id
5
+ def razorpay_order_id
6
+ external_id
7
+ end
8
+
9
+ # Helper to extract the signature if we need it later
10
+ def razorpay_signature
11
+ external_data&.dig('razorpay_signature')
12
+ end
13
+
14
+ # Mirrors Stripe's approach to safely generate the Spree::Payment
15
+ def find_or_create_payment!(razorpay_payment_id)
16
+ return unless persisted?
17
+ return payment if payment.present?
18
+
19
+ # Create the Spree::Payment record in the checkout state
20
+ order.payments.create!(
21
+ amount: amount,
22
+ payment_method: payment_method,
23
+ response_code: razorpay_payment_id,
24
+ source: create_source(razorpay_payment_id), # Optional: if you still use a source model
25
+ state: 'checkout'
26
+ )
27
+ end
28
+
29
+ private
30
+
31
+ # Optional: Only needed if you want to keep using your custom source model
32
+ def create_source(payment_id)
33
+ ::Spree::RazorpayCheckout.create!(
34
+ order_id: order.id,
35
+ razorpay_order_id: razorpay_order_id,
36
+ razorpay_payment_id: payment_id,
37
+ payment_method: payment_method.name,
38
+ status: 'captured'
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
@@ -3,6 +3,7 @@ module Spree
3
3
  self.table_name = 'spree_razorpay_checkouts'
4
4
 
5
5
  belongs_to :order, class_name: 'Spree::Order', optional: true
6
+ attr_accessor :user_id, :payment_method_id
6
7
 
7
8
  def name
8
9
  "Razorpay Secure (UPI, Wallets, Cards & Netbanking)"
@@ -6,9 +6,17 @@ module Razorpay
6
6
  def create(order_id)
7
7
  @order = Spree::Order.find_by(id: order_id)
8
8
  raise "Order not found" unless order
9
+
10
+ Razorpay.headers = {
11
+ "Content-Type" => "application/json",
12
+ "Accept" => "application/json"
13
+ }
14
+
9
15
  params = order_create_params
10
16
  Rails.logger.info "Razorpay::Order.create Params: #{params.inspect}"
11
- razorpay_order = Razorpay::Order.create(params)
17
+
18
+ razorpay_order = Razorpay::Order.create(params.to_json)
19
+
12
20
  if razorpay_order.try(:id).present?
13
21
  log_order_in_db(razorpay_order.id)
14
22
  return [razorpay_order.id, params[:amount]]
@@ -1,6 +1,6 @@
1
1
  <div class="alert alert-info">
2
2
  <p class="mb-0">
3
3
  To find your <strong>Key</strong> and <strong>Key Secret</strong>, go to the
4
- <%= external_link_to 'Razorpay dashboard', 'https://dashboard.razorpay.com/app/website-app-settings/api-keys', class: 'alert-link' %>
4
+ <%= external_link_to 'Razorpay dashboard', 'https://dashboard.razorpay.com/app/website-app-settings/api-keys', class: 'alert-link' %><br><%= external_link_to 'Create Webhook', 'https://dashboard.razorpay.com/app/website-app-settings/webhooks', class: 'alert-link' %> and mark all checkboxes in <strong>Active Events.</strong>
5
5
  </p>
6
6
  </div>
@@ -6,6 +6,7 @@
6
6
  <%= payment_method_icon_tag 'visa', class: 'm-1' %>
7
7
  <%= payment_method_icon_tag 'master', class: 'm-1' %>
8
8
  <%= payment_method_icon_tag 'google_pay', class: 'm-1' %>
9
+ <%= payment_method_icon_tag 'apple_pay', class: 'm-1' %>
9
10
  <%= payment_method_icon_tag 'amazon', class: 'm-1' %>
10
11
  <%= payment_method_icon_tag 'upi', class: 'm-1' %>
11
12
  </div>