spree_vpago 2.3.8 → 2.4.0.pre.pre1
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/Gemfile.lock +1 -1
- data/app/assets/javascripts/vpago/vpago_payments/user_informers/firebase.js +12 -9
- data/app/controllers/spree/miniapp/acledas_controller.rb +29 -0
- data/app/controllers/spree/vpago_payments_controller.rb +15 -9
- data/app/javascripts/vpago/vpago_payments/user_informers/firebase.js +11 -7
- data/app/models/spree/gateway/acleda_mini_app.rb +20 -0
- data/app/models/vpago/payment_decorator.rb +4 -0
- data/app/models/vpago/payment_method_decorator.rb +6 -0
- data/app/services/vpago/acleda_mini_app/session/create.rb +99 -0
- data/app/views/spree/admin/payments/source_views/_acleda_mini_app.html.erb +6 -0
- data/app/views/spree/vpago_payments/forms/spree/gateway/_acleda_mini_app.html.erb +93 -0
- data/config/routes.rb +7 -0
- data/lib/spree_vpago/engine.rb +1 -0
- data/lib/spree_vpago/version.rb +1 -1
- data/lib/vpago/acleda_mini_app/checkout.rb +23 -0
- metadata +10 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2b281c3ae420620dbd3d1da80ed2bc713c9283292484c1812cd1e9c979e6bf52
|
|
4
|
+
data.tar.gz: 70b7603d37c2c5e8f434e1ab333e622619296fd1ced75642ea9c0512a7591eb1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 718d7eb47ff0a91b7cbcd43d13bd28f4808e008bed9ebee489c913af2d18d5069b5bdc4f06cca8e57693559e96da51a7af8c6e8843e5e01952898c7853c919e1
|
|
7
|
+
data.tar.gz: dd5f68ec20858da961d36f88a488bd3906d0623baa9f88a3811bbe10957ebd93bf891d047df9b2b8123a549d6881ef8ef32e1c7674144e6dd7901c779e79ae8b
|
data/Gemfile.lock
CHANGED
|
@@ -13693,10 +13693,10 @@ This typically indicates that your device does not have a healthy Internet conne
|
|
|
13693
13693
|
firebaseConfigs,
|
|
13694
13694
|
documentReferencePath,
|
|
13695
13695
|
onPaymentIsProcessing,
|
|
13696
|
+
onPaymentIsRetrying,
|
|
13696
13697
|
onOrderIsProcessing,
|
|
13697
13698
|
onOrderIsCompleted,
|
|
13698
13699
|
onOrderProcessFailed,
|
|
13699
|
-
onPaymentIsRefunded,
|
|
13700
13700
|
onPaymentProcessFailed,
|
|
13701
13701
|
onCompleted
|
|
13702
13702
|
}) {
|
|
@@ -13713,6 +13713,9 @@ This typically indicates that your device does not have a healthy Internet conne
|
|
|
13713
13713
|
let processing = documentData["processing"] === true;
|
|
13714
13714
|
let reasonCode = documentData["reason_code"];
|
|
13715
13715
|
let reasonMessage = documentData["reason_message"];
|
|
13716
|
+
if (window.parent !== window) {
|
|
13717
|
+
window.parent.postMessage(messageCode, "*");
|
|
13718
|
+
}
|
|
13716
13719
|
let orderCompleted = orderState === "complete";
|
|
13717
13720
|
if (orderCompleted) {
|
|
13718
13721
|
queueProcessor.queueStateChange({
|
|
@@ -13743,11 +13746,11 @@ This typically indicates that your device does not have a healthy Internet conne
|
|
|
13743
13746
|
}
|
|
13744
13747
|
});
|
|
13745
13748
|
break;
|
|
13746
|
-
case "
|
|
13749
|
+
case "payment_is_retrying":
|
|
13747
13750
|
queueProcessor.queueStateChange({
|
|
13748
13751
|
minDelayInMs: 1500,
|
|
13749
13752
|
callback: async () => {
|
|
13750
|
-
await
|
|
13753
|
+
await onPaymentIsRetrying(
|
|
13751
13754
|
orderState,
|
|
13752
13755
|
paymentState,
|
|
13753
13756
|
processing,
|
|
@@ -13757,11 +13760,11 @@ This typically indicates that your device does not have a healthy Internet conne
|
|
|
13757
13760
|
}
|
|
13758
13761
|
});
|
|
13759
13762
|
break;
|
|
13760
|
-
case "
|
|
13763
|
+
case "order_is_processing":
|
|
13761
13764
|
queueProcessor.queueStateChange({
|
|
13762
13765
|
minDelayInMs: 1500,
|
|
13763
13766
|
callback: async () => {
|
|
13764
|
-
await
|
|
13767
|
+
await onOrderIsProcessing(
|
|
13765
13768
|
orderState,
|
|
13766
13769
|
paymentState,
|
|
13767
13770
|
processing,
|
|
@@ -13771,11 +13774,11 @@ This typically indicates that your device does not have a healthy Internet conne
|
|
|
13771
13774
|
}
|
|
13772
13775
|
});
|
|
13773
13776
|
break;
|
|
13774
|
-
case "
|
|
13777
|
+
case "order_is_completed":
|
|
13775
13778
|
queueProcessor.queueStateChange({
|
|
13776
13779
|
minDelayInMs: 1500,
|
|
13777
13780
|
callback: async () => {
|
|
13778
|
-
await
|
|
13781
|
+
await onOrderIsCompleted(
|
|
13779
13782
|
orderState,
|
|
13780
13783
|
paymentState,
|
|
13781
13784
|
processing,
|
|
@@ -13785,11 +13788,11 @@ This typically indicates that your device does not have a healthy Internet conne
|
|
|
13785
13788
|
}
|
|
13786
13789
|
});
|
|
13787
13790
|
break;
|
|
13788
|
-
case "
|
|
13791
|
+
case "order_process_failed":
|
|
13789
13792
|
queueProcessor.queueStateChange({
|
|
13790
13793
|
minDelayInMs: 1500,
|
|
13791
13794
|
callback: async () => {
|
|
13792
|
-
await
|
|
13795
|
+
await onOrderProcessFailed(
|
|
13793
13796
|
orderState,
|
|
13794
13797
|
paymentState,
|
|
13795
13798
|
processing,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Miniapp
|
|
3
|
+
# Partner Session Initialization API (ACLEDA Mini App Integration Spec, Step 01).
|
|
4
|
+
# ACLEDA (server-to-server) POSTs { phone, first_name, last_name }; we return a
|
|
5
|
+
# session-based miniAppUrl for ACLEDA to open in the WebView.
|
|
6
|
+
class AcledasController < Spree::Api::V2::BaseController
|
|
7
|
+
# POST /miniapp/acleda
|
|
8
|
+
def create
|
|
9
|
+
result = ::Vpago::AcledaMiniApp::Session::Create.call(
|
|
10
|
+
phone: session_params[:phone],
|
|
11
|
+
first_name: session_params[:first_name],
|
|
12
|
+
last_name: session_params[:last_name]
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if result.success?
|
|
16
|
+
render json: { message: 'SUCCESS', miniAppUrl: result.value[:mini_app_url] }
|
|
17
|
+
else
|
|
18
|
+
render json: { message: 'FAILED', error: result.error.to_s }, status: :unprocessable_entity
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def session_params
|
|
25
|
+
params.permit(:phone, :first_name, :last_name)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -8,11 +8,11 @@ module Spree
|
|
|
8
8
|
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
|
|
9
9
|
rescue_from CanCan::AccessDenied, with: :access_denied
|
|
10
10
|
|
|
11
|
+
before_action :find_payment, only: %i[checkout processing success]
|
|
12
|
+
after_action :allow_iframe_embedding, only: %i[checkout processing success]
|
|
13
|
+
|
|
11
14
|
# GET
|
|
12
15
|
def checkout
|
|
13
|
-
@payment = Vpago::PaymentFinder.new(params.permit!.to_h).find_and_verify
|
|
14
|
-
raise ActiveRecord::RecordNotFound unless @payment.present?
|
|
15
|
-
|
|
16
16
|
return redirect_to @payment.processing_url, allow_other_host: true unless @payment.checkout?
|
|
17
17
|
|
|
18
18
|
@order = @payment.order
|
|
@@ -22,9 +22,6 @@ module Spree
|
|
|
22
22
|
|
|
23
23
|
# GET
|
|
24
24
|
def processing
|
|
25
|
-
@payment = Vpago::PaymentFinder.new(params.permit!.to_h).find_and_verify
|
|
26
|
-
raise ActiveRecord::RecordNotFound unless @payment.present?
|
|
27
|
-
|
|
28
25
|
@order = @payment.order
|
|
29
26
|
|
|
30
27
|
VpagoLogger.log(label: 'Spree::VpagoPaymentsController#processing', data: vpago_log_context)
|
|
@@ -32,9 +29,6 @@ module Spree
|
|
|
32
29
|
|
|
33
30
|
# GET
|
|
34
31
|
def success
|
|
35
|
-
@payment = Vpago::PaymentFinder.new(params.permit!.to_h).find_and_verify
|
|
36
|
-
raise ActiveRecord::RecordNotFound unless @payment.present?
|
|
37
|
-
|
|
38
32
|
@order = @payment.order
|
|
39
33
|
raise CanCan::AccessDenied unless @order.completed?
|
|
40
34
|
|
|
@@ -147,6 +141,18 @@ module Spree
|
|
|
147
141
|
sanitized_params
|
|
148
142
|
end
|
|
149
143
|
|
|
144
|
+
def find_payment
|
|
145
|
+
@payment = Vpago::PaymentFinder.new(params.permit!.to_h).find_and_verify
|
|
146
|
+
raise ActiveRecord::RecordNotFound unless @payment.present?
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# frame-ancestors (set above) supersedes X-Frame-Options in modern
|
|
150
|
+
# browsers, but the default SAMEORIGIN header would still block legacy
|
|
151
|
+
# browsers from embedding these pages, so drop it for the iframe actions.
|
|
152
|
+
def allow_iframe_embedding
|
|
153
|
+
response.headers.delete('X-Frame-Options')
|
|
154
|
+
end
|
|
155
|
+
|
|
150
156
|
def render_not_found
|
|
151
157
|
respond_to do |format|
|
|
152
158
|
format.html { render file: Rails.public_path.join('404.html'), status: :not_found, layout: false }
|
|
@@ -31,6 +31,10 @@ async function listenToProcessingState({
|
|
|
31
31
|
let reasonCode = documentData["reason_code"];
|
|
32
32
|
let reasonMessage = documentData["reason_message"];
|
|
33
33
|
|
|
34
|
+
if (window.parent !== window) {
|
|
35
|
+
window.parent.postMessage(messageCode, "*");
|
|
36
|
+
}
|
|
37
|
+
|
|
34
38
|
let orderCompleted = orderState === "complete";
|
|
35
39
|
if (orderCompleted) {
|
|
36
40
|
queueProcessor.queueStateChange({
|
|
@@ -40,7 +44,7 @@ async function listenToProcessingState({
|
|
|
40
44
|
orderState,
|
|
41
45
|
paymentState,
|
|
42
46
|
reasonCode,
|
|
43
|
-
reasonMessage
|
|
47
|
+
reasonMessage,
|
|
44
48
|
);
|
|
45
49
|
},
|
|
46
50
|
});
|
|
@@ -57,7 +61,7 @@ async function listenToProcessingState({
|
|
|
57
61
|
paymentState,
|
|
58
62
|
processing,
|
|
59
63
|
reasonCode,
|
|
60
|
-
reasonMessage
|
|
64
|
+
reasonMessage,
|
|
61
65
|
);
|
|
62
66
|
},
|
|
63
67
|
});
|
|
@@ -71,7 +75,7 @@ async function listenToProcessingState({
|
|
|
71
75
|
paymentState,
|
|
72
76
|
processing,
|
|
73
77
|
reasonCode,
|
|
74
|
-
reasonMessage
|
|
78
|
+
reasonMessage,
|
|
75
79
|
);
|
|
76
80
|
},
|
|
77
81
|
});
|
|
@@ -85,7 +89,7 @@ async function listenToProcessingState({
|
|
|
85
89
|
paymentState,
|
|
86
90
|
processing,
|
|
87
91
|
reasonCode,
|
|
88
|
-
reasonMessage
|
|
92
|
+
reasonMessage,
|
|
89
93
|
);
|
|
90
94
|
},
|
|
91
95
|
});
|
|
@@ -99,7 +103,7 @@ async function listenToProcessingState({
|
|
|
99
103
|
paymentState,
|
|
100
104
|
processing,
|
|
101
105
|
reasonCode,
|
|
102
|
-
reasonMessage
|
|
106
|
+
reasonMessage,
|
|
103
107
|
);
|
|
104
108
|
},
|
|
105
109
|
});
|
|
@@ -113,7 +117,7 @@ async function listenToProcessingState({
|
|
|
113
117
|
paymentState,
|
|
114
118
|
processing,
|
|
115
119
|
reasonCode,
|
|
116
|
-
reasonMessage
|
|
120
|
+
reasonMessage,
|
|
117
121
|
);
|
|
118
122
|
},
|
|
119
123
|
});
|
|
@@ -127,7 +131,7 @@ async function listenToProcessingState({
|
|
|
127
131
|
paymentState,
|
|
128
132
|
processing,
|
|
129
133
|
reasonCode,
|
|
130
|
-
reasonMessage
|
|
134
|
+
reasonMessage,
|
|
131
135
|
);
|
|
132
136
|
},
|
|
133
137
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# ACLEDA Mini App payment method.
|
|
3
|
+
#
|
|
4
|
+
# Reuses the ACLEDA V2 integration in deeplink mode: the deeplink produced by
|
|
5
|
+
# ACLEDA's openSessionV2 API is handed to the ACLEDA super-app through the
|
|
6
|
+
# `acledaJSBridge` (startPayment) instead of redirecting the page. Server-side
|
|
7
|
+
# verification reuses AcledaV2's #check_transaction (getTxnStatus).
|
|
8
|
+
class Gateway::AcledaMiniApp < Gateway::AcledaV2
|
|
9
|
+
# override: partial to render in admin / checkout form lookup
|
|
10
|
+
def method_type
|
|
11
|
+
'acleda_mini_app'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The mini app always uses the deeplink + JS bridge, so admins don't need to
|
|
15
|
+
# set the ACLEDA V2 mode.
|
|
16
|
+
def deeplink?
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -6,6 +6,7 @@ module Vpago
|
|
|
6
6
|
TYPE_ACLEDA = 'Spree::Gateway::Acleda'.freeze
|
|
7
7
|
TYPE_ACLEDA_V2 = 'Spree::Gateway::AcledaV2'.freeze
|
|
8
8
|
TYPE_ACLEDA_MOBILE = 'Spree::Gateway::AcledaMobile'.freeze
|
|
9
|
+
TYPE_ACLEDA_MINI_APP = 'Spree::Gateway::AcledaMiniApp'.freeze
|
|
9
10
|
TYPE_TRUE_MONEY = 'Spree::Gateway::TrueMoney'.freeze
|
|
10
11
|
TYPE_VATTANAC = 'Spree::Gateway::Vattanac'.freeze
|
|
11
12
|
TYPE_VATTANAC_MINI_APP = 'Spree::Gateway::VattanacMiniApp'.freeze
|
|
@@ -24,6 +25,7 @@ module Vpago
|
|
|
24
25
|
Spree::PaymentMethod::TYPE_ACLEDA,
|
|
25
26
|
Spree::PaymentMethod::TYPE_ACLEDA_V2,
|
|
26
27
|
Spree::PaymentMethod::TYPE_ACLEDA_MOBILE,
|
|
28
|
+
Spree::PaymentMethod::TYPE_ACLEDA_MINI_APP,
|
|
27
29
|
Spree::PaymentMethod::TYPE_VATTANAC,
|
|
28
30
|
Spree::PaymentMethod::TYPE_VATTANAC_MINI_APP,
|
|
29
31
|
Spree::PaymentMethod::TYPE_TRUE_MONEY,
|
|
@@ -95,6 +97,10 @@ module Vpago
|
|
|
95
97
|
type == Spree::PaymentMethod::TYPE_ACLEDA_V2
|
|
96
98
|
end
|
|
97
99
|
|
|
100
|
+
def type_acleda_mini_app?
|
|
101
|
+
type == Spree::PaymentMethod::TYPE_ACLEDA_MINI_APP
|
|
102
|
+
end
|
|
103
|
+
|
|
98
104
|
def type_payway?
|
|
99
105
|
type == Spree::PaymentMethod::TYPE_PAYWAY
|
|
100
106
|
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module Vpago
|
|
2
|
+
module AcledaMiniApp
|
|
3
|
+
module Session
|
|
4
|
+
# Partner Session Initialization (ACLEDA Mini App Integration Spec, Step 01).
|
|
5
|
+
#
|
|
6
|
+
# ACLEDA calls POST /miniapp/acleda with { phone, first_name, last_name }.
|
|
7
|
+
# We find or create the customer (tagged with the acleda_bank identity),
|
|
8
|
+
# mint a session JWT, and return a session-based miniAppUrl for ACLEDA to
|
|
9
|
+
# open in the WebView. The generic session_id OAuth grant validates the JWT
|
|
10
|
+
# when the WebView later authenticates.
|
|
11
|
+
class Create
|
|
12
|
+
prepend Spree::ServiceModule::Base
|
|
13
|
+
|
|
14
|
+
def call(phone: nil, first_name: nil, last_name: nil)
|
|
15
|
+
return failure(nil, 'phone is required') if phone.blank?
|
|
16
|
+
|
|
17
|
+
@phone = phone
|
|
18
|
+
@first_name = first_name
|
|
19
|
+
@last_name = last_name
|
|
20
|
+
|
|
21
|
+
user = find_or_create_user
|
|
22
|
+
return failure(user, 'user creation failed') if user.nil? || !user.persisted?
|
|
23
|
+
|
|
24
|
+
success(mini_app_url: mini_app_url(user))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :phone, :first_name, :last_name
|
|
30
|
+
|
|
31
|
+
def find_or_create_user
|
|
32
|
+
identity = acleda_identity
|
|
33
|
+
|
|
34
|
+
# Returning customer: the ACLEDA identity already points at a user.
|
|
35
|
+
return identity.user if identity.persisted?
|
|
36
|
+
|
|
37
|
+
# New to ACLEDA: reuse a customer matched by phone, otherwise build one,
|
|
38
|
+
# then tag them with the acleda_bank identity so the lookup above hits
|
|
39
|
+
# next time.
|
|
40
|
+
user = find_existing_user || build_user
|
|
41
|
+
identity.name = full_name
|
|
42
|
+
user.user_identity_providers << identity
|
|
43
|
+
user.save
|
|
44
|
+
|
|
45
|
+
user
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def acleda_identity
|
|
49
|
+
SpreeCmCommissioner::UserIdentityProvider.acleda_bank.find_or_initialize_by(sub: phone)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def find_existing_user
|
|
53
|
+
return if intel_phone.blank?
|
|
54
|
+
|
|
55
|
+
Spree::User.by_non_tenant.find_by(intel_phone_number: intel_phone)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_user
|
|
59
|
+
Spree::User.new(
|
|
60
|
+
first_name: first_name,
|
|
61
|
+
last_name: last_name,
|
|
62
|
+
phone_number: phone,
|
|
63
|
+
intel_phone_number: intel_phone,
|
|
64
|
+
password: SecureRandom.base64(16),
|
|
65
|
+
confirmed_at: Time.zone.now
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def mini_app_url(user)
|
|
70
|
+
# A JWT signed with the user's secure_token. Passed as `session_key` (not
|
|
71
|
+
# `session_id`) so the OAuth grant routes it to the ACLEDA authenticator,
|
|
72
|
+
# which enforces the signature.
|
|
73
|
+
session_key = SpreeCmCommissioner::UserSessionJwtToken.encode(
|
|
74
|
+
{ user_id: user.id },
|
|
75
|
+
user.reload.secure_token
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
"#{Spree::Store.default.formatted_url}/mini_app/acleda?session_key=#{session_key}&ds=#{CGI.escape(disable_services)}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def full_name
|
|
82
|
+
[first_name, last_name].compact.join(' ').presence
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def intel_phone
|
|
86
|
+
return @intel_phone if defined?(@intel_phone)
|
|
87
|
+
|
|
88
|
+
@intel_phone = SpreeCmCommissioner::PhoneNumberParser.call(phone_number: phone).intel_phone_number
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Services to disable in the mini app storefront. Configurable per ACLEDA's
|
|
92
|
+
# requirement; defaults to none.
|
|
93
|
+
def disable_services
|
|
94
|
+
ENV.fetch('ACLEDA_MINI_APP_DISABLE_SERVICES', '')
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<%
|
|
2
|
+
opr_device = request.user_agent.to_s.downcase.match?(/iphone|ipad|ipod/) ? 'ios' : 'android'
|
|
3
|
+
checkout = ::Vpago::AcledaMiniApp::Checkout.new(@payment, { platform: 'app', opr_device: opr_device })
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
checkout.call
|
|
7
|
+
rescue => e
|
|
8
|
+
Rails.logger.error("Acleda Mini App checkout.call failed: #{e.message}")
|
|
9
|
+
end
|
|
10
|
+
%>
|
|
11
|
+
|
|
12
|
+
<p id="unsupported-message"
|
|
13
|
+
style="margin-top: 10px; font-weight: bold; color: red; display: none;">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<% if checkout.failed? %>
|
|
17
|
+
<script>
|
|
18
|
+
console.error("[Vpago] Acleda Mini App checkout error: <%= j checkout.error_message.to_s %>");
|
|
19
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
20
|
+
const loadingContainer = document.getElementById('vpago_loading_container');
|
|
21
|
+
if (loadingContainer) {
|
|
22
|
+
loadingContainer.textContent = "We couldn't start your payment. Please go back and try again.";
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
<% else %>
|
|
27
|
+
<script>
|
|
28
|
+
// ACLEDA JS bridge (unified iOS + Android). Defined before use so it is
|
|
29
|
+
// available when the native layer injects its handlers.
|
|
30
|
+
window.acledaJSBridge = window.acledaJSBridge || {
|
|
31
|
+
call: function (action, data = {}) {
|
|
32
|
+
const payload = { action, data };
|
|
33
|
+
|
|
34
|
+
// iOS WKWebView
|
|
35
|
+
if (
|
|
36
|
+
window.webkit &&
|
|
37
|
+
window.webkit.messageHandlers &&
|
|
38
|
+
window.webkit.messageHandlers.acledaIOSJSBridge
|
|
39
|
+
) {
|
|
40
|
+
window.webkit.messageHandlers.acledaIOSJSBridge.postMessage(payload);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Android WebView
|
|
45
|
+
if (
|
|
46
|
+
window.acledaAndroidJSBridge &&
|
|
47
|
+
typeof window.acledaAndroidJSBridge.postMessage === "function"
|
|
48
|
+
) {
|
|
49
|
+
window.acledaAndroidJSBridge.postMessage(JSON.stringify(payload));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.warn("[acledaJSBridge] Native bridge not found");
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function showRedirectContainer(buttonLabel, onClick) {
|
|
59
|
+
const loadingContainer = document.getElementById('vpago_loading_container');
|
|
60
|
+
const redirectContainer = document.getElementById('vpago_redirect_container');
|
|
61
|
+
const redirectButton = document.getElementById('vpago_redirect_button');
|
|
62
|
+
|
|
63
|
+
redirectButton.textContent = buttonLabel;
|
|
64
|
+
redirectButton.addEventListener('click', onClick);
|
|
65
|
+
|
|
66
|
+
loadingContainer.style.display = 'none';
|
|
67
|
+
redirectContainer.style.removeProperty('display');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
71
|
+
const acledaDeeplink = "<%= raw checkout.acleda_deeplink %>";
|
|
72
|
+
const processingUrl = "<%= raw @payment.processing_url %>";
|
|
73
|
+
|
|
74
|
+
// Native -> JS after payment completes. Primary completion signal; the
|
|
75
|
+
// transaction checker polling is the fallback.
|
|
76
|
+
window.onPaymentResult = function (result) {
|
|
77
|
+
console.log("[acledaJSBridge] Payment result:", result);
|
|
78
|
+
// result.batchId, result.status = "success" | "failed" | "cancelled"
|
|
79
|
+
window.location.href = processingUrl;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const startPayment = () => window.acledaJSBridge.call("startPayment", { acleda_deeplink: acledaDeeplink });
|
|
83
|
+
|
|
84
|
+
if (startPayment()) {
|
|
85
|
+
showRedirectContainer("Continue", startPayment);
|
|
86
|
+
} else {
|
|
87
|
+
const unsupported = document.getElementById('unsupported-message');
|
|
88
|
+
unsupported.style.display = 'block';
|
|
89
|
+
unsupported.innerText = 'Unsupported environment';
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
</script>
|
|
93
|
+
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -17,6 +17,13 @@ Spree::Core::Engine.add_routes do
|
|
|
17
17
|
resources :transactions, only: [:show]
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# ACLEDA Mini App session init (ACLEDA Mini App Integration Spec, Step 01).
|
|
21
|
+
# ACLEDA calls POST /miniapp/acleda with { phone, first_name, last_name }
|
|
22
|
+
# and receives a session-based miniAppUrl.
|
|
23
|
+
namespace :miniapp do
|
|
24
|
+
resources :acleda, only: [:create], controller: 'acledas'
|
|
25
|
+
end
|
|
26
|
+
|
|
20
27
|
resource :vpago_payments do
|
|
21
28
|
collection do
|
|
22
29
|
get :checkout
|
data/lib/spree_vpago/engine.rb
CHANGED
data/lib/spree_vpago/version.rb
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Vpago
|
|
2
|
+
module AcledaMiniApp
|
|
3
|
+
# ACLEDA Mini App checkout. Reuses ACLEDA V2's openSessionV2 flow to obtain a
|
|
4
|
+
# deeplink, which is passed to the ACLEDA super-app via the acledaJSBridge
|
|
5
|
+
# (startPayment) instead of redirecting the page.
|
|
6
|
+
class Checkout < Vpago::AcledaV2::Checkout
|
|
7
|
+
# The mini app always uses the deeplink + JS bridge, regardless of the
|
|
8
|
+
# gateway's configured mode.
|
|
9
|
+
def deeplink?
|
|
10
|
+
true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The deeplink string handed to
|
|
14
|
+
# acledaJSBridge.call('startPayment', { acleda_deeplink: ... })
|
|
15
|
+
# Ensures openSessionV2 has run (persisting paymentTokenid for later
|
|
16
|
+
# getTxnStatus verification) before returning the deeplinkUrl.
|
|
17
|
+
def acleda_deeplink
|
|
18
|
+
call if @response.nil?
|
|
19
|
+
deeplink_url
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_vpago
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.4.0.pre.pre1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -167,6 +167,7 @@ files:
|
|
|
167
167
|
- app/controllers/spree/admin/payouts_controller.rb
|
|
168
168
|
- app/controllers/spree/admin/suspicious_orders_controller.rb
|
|
169
169
|
- app/controllers/spree/api/v2/storefront/checkout_controller_decorator.rb
|
|
170
|
+
- app/controllers/spree/miniapp/acledas_controller.rb
|
|
170
171
|
- app/controllers/spree/payway_card_popups_controller.rb
|
|
171
172
|
- app/controllers/spree/payway_results_controller.rb
|
|
172
173
|
- app/controllers/spree/payway_v2_card_popups_controller.rb
|
|
@@ -192,6 +193,7 @@ files:
|
|
|
192
193
|
- app/models/.gitkeep
|
|
193
194
|
- app/models/concerns/vpago/payoutable.rb
|
|
194
195
|
- app/models/spree/gateway/acleda.rb
|
|
196
|
+
- app/models/spree/gateway/acleda_mini_app.rb
|
|
195
197
|
- app/models/spree/gateway/acleda_mobile.rb
|
|
196
198
|
- app/models/spree/gateway/acleda_v2.rb
|
|
197
199
|
- app/models/spree/gateway/payway.rb
|
|
@@ -256,6 +258,7 @@ files:
|
|
|
256
258
|
- app/serializers/spree/v2/storefront/payment_serializer_decorator.rb
|
|
257
259
|
- app/serializers/spree/v2/storefront/vpago_payment_source_serializer.rb
|
|
258
260
|
- app/services/.gitkeep
|
|
261
|
+
- app/services/vpago/acleda_mini_app/session/create.rb
|
|
259
262
|
- app/services/vpago/aes_encrypter.rb
|
|
260
263
|
- app/services/vpago/payment_finder.rb
|
|
261
264
|
- app/services/vpago/payment_processable.rb
|
|
@@ -283,6 +286,7 @@ files:
|
|
|
283
286
|
- app/views/spree/admin/payments/source_forms/_payment_payway.html.erb
|
|
284
287
|
- app/views/spree/admin/payments/source_forms/_payway_v2.html.erb
|
|
285
288
|
- app/views/spree/admin/payments/source_views/_acleda.html.erb
|
|
289
|
+
- app/views/spree/admin/payments/source_views/_acleda_mini_app.html.erb
|
|
286
290
|
- app/views/spree/admin/payments/source_views/_acleda_mobile.html.erb
|
|
287
291
|
- app/views/spree/admin/payments/source_views/_acleda_v2.html.erb
|
|
288
292
|
- app/views/spree/admin/payments/source_views/_payment_payway.html.erb
|
|
@@ -340,6 +344,7 @@ files:
|
|
|
340
344
|
- app/views/spree/payway_v2_card_popups/show.html.erb
|
|
341
345
|
- app/views/spree/vpago_payments/_transaction_checker.html.erb
|
|
342
346
|
- app/views/spree/vpago_payments/checkout.html.erb
|
|
347
|
+
- app/views/spree/vpago_payments/forms/spree/gateway/_acleda_mini_app.html.erb
|
|
343
348
|
- app/views/spree/vpago_payments/forms/spree/gateway/_acleda_v2.html.erb
|
|
344
349
|
- app/views/spree/vpago_payments/forms/spree/gateway/_payway_v2.html.erb
|
|
345
350
|
- app/views/spree/vpago_payments/forms/spree/gateway/_true_money.html.erb
|
|
@@ -391,6 +396,7 @@ files:
|
|
|
391
396
|
- lib/vpago/acleda/deeplink_checkout.rb
|
|
392
397
|
- lib/vpago/acleda/payment_request_updater.rb
|
|
393
398
|
- lib/vpago/acleda/transaction_status.rb
|
|
399
|
+
- lib/vpago/acleda_mini_app/checkout.rb
|
|
394
400
|
- lib/vpago/acleda_mobile/base.rb
|
|
395
401
|
- lib/vpago/acleda_mobile/callback_validator.php
|
|
396
402
|
- lib/vpago/acleda_mobile/callback_validator.rb
|
|
@@ -456,9 +462,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
456
462
|
version: 3.2.0
|
|
457
463
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
458
464
|
requirements:
|
|
459
|
-
- - "
|
|
465
|
+
- - ">"
|
|
460
466
|
- !ruby/object:Gem::Version
|
|
461
|
-
version:
|
|
467
|
+
version: 1.3.1
|
|
462
468
|
requirements:
|
|
463
469
|
- none
|
|
464
470
|
rubygems_version: 3.4.1
|