PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.19
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.19
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / app / Modules / PaymentMethods / AirwallexGateway / Airwallex.php

Airwallex.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.19, at app/Modules/PaymentMethods/AirwallexGateway/Airwallex.php

481 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\PaymentMethods\AirwallexGateway;
4
5 use FluentCart\App\App;
6 use FluentCart\App\Modules\PaymentMethods\Core\AbstractPaymentGateway;
7 use FluentCart\App\Services\Payments\PaymentInstance;
8 use FluentCart\App\Vite;
9 use FluentCart\Framework\Support\Arr;
10
11 class Airwallex extends AbstractPaymentGateway
12 {
13 public array $supportedFeatures = [
14 'payment',
15 'refund',
16 'webhook'
17 ];
18
19 public function __construct()
20 {
21 parent::__construct(new AirwallexSettingsBase());
22 }
23
24 public function meta(): array
25 {
26 return [
27 'title' => __('Airwallex', 'fluent-cart'),
28 'route' => 'airwallex',
29 'description' => __('Pay securely with Airwallex - Global payment processing', 'fluent-cart'),
30 'logo' => Vite::getAssetUrl("images/payment-methods/airwallex-logo.svg"),
31 'icon' => Vite::getAssetUrl("images/payment-methods/airwallex-logo.svg"),
32 'brand_color' => '#6c5ce7',
33 'status' => $this->settings->get('is_active') === 'yes',
34 'upcoming' => true,
35 ];
36 }
37
38 public function boot()
39 {
40 // init IPN related class/actions here
41 add_filter('fluent_cart/payment_methods/airwallex_settings', [$this, 'getSettings'], 10, 2);
42 }
43
44 public function makePaymentFromPaymentInstance(PaymentInstance $paymentInstance)
45 {
46 // todo: implement in future
47 die();
48 }
49
50 public function refund($refundInfo, $order, $transaction)
51 {
52 // todo: implement in future
53 die();
54 }
55
56 public function handleIPN(): void
57 {
58 $payload = json_decode(file_get_contents('php://input'), true); // will get from request after verification
59 // Verify webhook signature
60 if (!$this->verifyWebhookSignature($payload)) {
61 http_response_code(401);
62 exit('Unauthorized');
63 }
64
65 $eventType = $payload['name'] ?? '';
66
67 switch ($eventType) {
68 case 'payment_intent.succeeded':
69 $this->handlePaymentSucceeded($payload);
70 break;
71 case 'payment_intent.failed':
72 $this->handlePaymentFailed($payload);
73 break;
74 case 'refund.received':
75 $this->handleRefundReceived($payload);
76 break;
77 }
78
79 http_response_code(200);
80 exit('OK');
81 }
82
83 public function getOrderInfo(array $data)
84 {
85 $items = $this->getCheckoutItems();
86
87 $subTotal = 0;
88 foreach ($items as $item) {
89 $subTotal += intval($item['quantity'] * $item['unit_price']);
90 }
91
92 $paymentArgs = [
93 'client_id' => $this->settings->getClientId(),
94 'amount' => $subTotal,
95 'currency' => $this->storeSettings->get('currency'),
96 ];
97
98 wp_send_json([
99 'status' => 'success',
100 'payment_args' => $paymentArgs,
101 'has_subscription' => false
102 ], 200);
103 }
104
105 public function fields()
106 {
107 $webhook_url = site_url() . '?fct_payment_listener=1&method=airwallex';
108 $webhook_instructions = sprintf(
109 '<div>
110 <p><b>%1$s</b><code class="copyable-content">%2$s</code></p>
111 <p>%3$s</p>
112 <br>
113 <h4>%4$s</h4>
114 <br>
115 <p>%5$s</p>
116 <p>%6$s <a href="https://www.airwallex.com/app/settings/developer" target="_blank">%7$s</a></p>
117 <p>%8$s <code class="copyable-content">%2$s</code></p>
118 <p>%9$s</p>
119 </div>',
120 __('Webhook URL: ', 'fluent-cart'), // %1$s
121 $webhook_url, // %2$s (reused)
122 __('You should configure your Airwallex webhooks to get all updates of your payments remotely.', 'fluent-cart'), // %3$s
123 __('How to configure?', 'fluent-cart'), // %4$s
124 __('In your Airwallex Console:', 'fluent-cart'), // %5$s
125 __('Go to Developers > Webhooks >', 'fluent-cart'), // %6$s
126 __('Add webhook', 'fluent-cart'), // %7$s
127 __('Enter The Webhook URL: ', 'fluent-cart'), // %8$s
128 __('Select payment events', 'fluent-cart') // %9$s
129 );
130
131 return array(
132 // 'notice' => [
133 // 'value' => $this->getStoreModeNotice(),
134 // 'label' => __('Store Mode notice', 'fluent-cart'),
135 // 'type' => 'notice'
136 // ],
137 'upcoming' => [
138 'value' => $this->isUpcoming(),
139 'label' => __('Payment method is upcoming!', 'fluent-cart'),
140 'type' => 'upcoming'
141 ],
142 'payment_mode' => [
143 'type' => 'tabs',
144 'schema' => [
145 [
146 'type' => 'tab',
147 'label' => __('Test credentials', 'fluent-cart'),
148 'value' => 'test',
149 'schema' => [
150 'test_client_id' => array(
151 'value' => '',
152 'label' => __('Test Client ID', 'fluent-cart'),
153 'type' => 'text',
154 'placeholder' => __('Your test client ID', 'fluent-cart'),
155 'dependency' => [
156 'depends_on' => 'payment_mode',
157 'operator' => '=',
158 'value' => 'test'
159 ]
160 ),
161 'test_api_key' => array(
162 'value' => '',
163 'label' => __('Test API Key', 'fluent-cart'),
164 'type' => 'password',
165 'placeholder' => __('Your test API key', 'fluent-cart'),
166 'dependency' => [
167 'depends_on' => 'payment_mode',
168 'operator' => '=',
169 'value' => 'test'
170 ]
171 ),
172 ],
173 ],
174 [
175 'type' => 'tab',
176 'label' => __('Live credentials', 'fluent-cart'),
177 'value' => 'live',
178 'schema' => [
179 'live_client_id' => array(
180 'value' => '',
181 'label' => __('Live Client ID', 'fluent-cart'),
182 'type' => 'text',
183 'placeholder' => __('Your live client ID', 'fluent-cart'),
184 'dependency' => [
185 'depends_on' => 'payment_mode',
186 'operator' => '=',
187 'value' => 'live'
188 ]
189 ),
190 'live_api_key' => array(
191 'value' => '',
192 'label' => __('Live API Key', 'fluent-cart'),
193 'type' => 'password',
194 'placeholder' => __('Your live API key', 'fluent-cart'),
195 'dependency' => [
196 'depends_on' => 'payment_mode',
197 'operator' => '=',
198 'value' => 'live'
199 ]
200 ),
201 ]
202 ]
203 ]
204 ],
205 'webhook_desc' => array(
206 'value' => $webhook_instructions,
207 'label' => __('Webhook URL', 'fluent-cart'),
208 'type' => 'html_attr'
209 ),
210 );
211 }
212
213 public static function validateSettings($data): array
214 {
215 $clientId = $data['client_id'] ?? '';
216 $apiKey = $data['api_key'] ?? '';
217
218 if (empty($clientId) || empty($apiKey)) {
219 return [
220 'status' => 'failed',
221 'message' => __('Client ID and API Key are required', 'fluent-cart')
222 ];
223 }
224
225 try {
226 $testResponse = static::testApiConnection($clientId, $apiKey, $data['payment_mode'] ?? 'demo');
227
228 return [
229 'status' => 'success',
230 'message' => __('Airwallex settings validated successfully', 'fluent-cart')
231 ];
232 } catch (\Exception $e) {
233 return [
234 'status' => 'failed',
235 'message' => $e->getMessage()
236 ];
237 }
238 }
239
240 public function getEnqueueScriptSrc($hasSubscription = 'no'): array
241 {
242 $mode = $this->settings->getMode();
243 $airwallexJsUrl = $mode === 'production'
244 ? 'https://checkout.airwallex.com/assets/elements.bundle.min.js'
245 : 'https://checkout.airwallex.com/assets/elements.bundle.min.js';
246
247 return [
248 [
249 'handle' => 'airwallex-elements-js',
250 'src' => $airwallexJsUrl,
251 ],
252 [
253 'handle' => 'fluent-cart-airwallex-checkout',
254 'src' => Vite::getEnqueuePath('public/payment-methods/airwallex-checkout.js'),
255 'deps' => ['airwallex-elements-js']
256 ]
257 ];
258 }
259
260 public function getEnqueueStyleSrc(): array
261 {
262 return [
263 [
264 'handle' => 'fluent-cart-airwallex-styles',
265 'src' => Vite::getEnqueuePath('public/payment-methods/airwallex.css'),
266 ]
267 ];
268 }
269
270 private function getAccessToken()
271 {
272 $clientId = $this->settings->getClientId();
273 $apiKey = $this->settings->getApiKey();
274 $baseUrl = $this->getApiBaseUrl();
275
276 $response = wp_remote_post("{$baseUrl}/api/v1/authentication/login", [
277 'headers' => [
278 'Content-Type' => 'application/json'
279 ],
280 'body' => json_encode([
281 'x-client-id' => $clientId,
282 'x-api-key' => $apiKey
283 ]),
284 'timeout' => 30
285 ]);
286
287 if (is_wp_error($response)) {
288 throw new \Exception(esc_html($response->get_error_message()));
289 }
290
291 $body = wp_remote_retrieve_body($response);
292 $data = json_decode($body, true);
293
294 if (wp_remote_retrieve_response_code($response) !== 201) {
295 throw new \Exception(esc_html__('Failed to authenticate with Airwallex', 'fluent-cart'));
296 }
297
298 return $data['token'];
299 }
300
301 private function createAirwallexPaymentIntent($data, $accessToken)
302 {
303 $baseUrl = $this->getApiBaseUrl();
304
305 $response = wp_remote_post("{$baseUrl}/api/v1/pa/payment_intents/create", [
306 'headers' => [
307 'Authorization' => 'Bearer ' . $accessToken,
308 'Content-Type' => 'application/json'
309 ],
310 'body' => json_encode($data),
311 'timeout' => 30
312 ]);
313
314 if (is_wp_error($response)) {
315 throw new \Exception(esc_html($response->get_error_message()));
316 }
317
318 $body = wp_remote_retrieve_body($response);
319 $responseData = json_decode($body, true);
320
321 if (wp_remote_retrieve_response_code($response) !== 201) {
322 $error = $responseData['message'] ?? 'Unknown error';
323 throw new \Exception(esc_html($error));
324 }
325
326 return $responseData;
327 }
328
329 private function processAirwallexRefund($refundData, $accessToken)
330 {
331 $baseUrl = $this->getApiBaseUrl();
332
333 $response = wp_remote_post("{$baseUrl}/api/v1/pa/refunds/create", [
334 'headers' => [
335 'Authorization' => 'Bearer ' . $accessToken,
336 'Content-Type' => 'application/json'
337 ],
338 'body' => json_encode($refundData),
339 'timeout' => 30
340 ]);
341
342 if (is_wp_error($response)) {
343 throw new \Exception(esc_html($response->get_error_message()));
344 }
345
346 $body = wp_remote_retrieve_body($response);
347 $responseData = json_decode($body, true);
348
349 if (wp_remote_retrieve_response_code($response) !== 201) {
350 $error = $responseData['message'] ?? 'Unknown error';
351 throw new \Exception(esc_html($error));
352 }
353
354 return $responseData;
355 }
356
357 private static function testApiConnection($clientId, $apiKey, $mode)
358 {
359 $baseUrl = $mode === 'production'
360 ? 'https://api.airwallex.com'
361 : 'https://api-demo.airwallex.com';
362
363 $response = wp_remote_post("{$baseUrl}/api/v1/authentication/login", [
364 'headers' => [
365 'Content-Type' => 'application/json'
366 ],
367 'body' => json_encode([
368 'x-client-id' => $clientId,
369 'x-api-key' => $apiKey
370 ]),
371 'timeout' => 30
372 ]);
373
374 if (is_wp_error($response)) {
375 throw new \Exception(esc_html($response->get_error_message()));
376 }
377
378 if (wp_remote_retrieve_response_code($response) !== 201) {
379 throw new \Exception(esc_html__('Invalid Airwallex credentials', 'fluent-cart'));
380 }
381
382 return true;
383 }
384
385 private function verifyWebhookSignature($payload)
386 {
387 $serverData = App::request()->server();
388 $signature = Arr::get($serverData, 'HTTP_X_SIGNATURE', '');
389 $timestamp = Arr::get($serverData, 'HTTP_X_TIMESTAMP', '');
390 $webhookSecret = $this->settings->getWebhookSecret();
391
392 if (!$signature || !$timestamp || !$webhookSecret) {
393 return false;
394 }
395
396 $signedPayload = $timestamp . json_encode($payload);
397 $expectedSignature = hash_hmac('sha256', $signedPayload, $webhookSecret);
398
399 return hash_equals($expectedSignature, $signature);
400 }
401
402 private function getApiBaseUrl()
403 {
404 return $this->settings->getMode() === 'production'
405 ? 'https://api.airwallex.com'
406 : 'https://api-demo.airwallex.com';
407 }
408
409 private function getWebhookUrl()
410 {
411 return site_url('?fluent_cart_payment_api_notify=airwallex');
412 }
413
414 public function webHookPaymentMethodName()
415 {
416 return $this->getMeta('route');
417 }
418
419 private function generateRequestId($order, $type = 'payment')
420 {
421 return $order->uuid . '_' . $type . '_' . time();
422 }
423
424 private function handlePaymentSucceeded($payload)
425 {
426 $paymentIntent = $payload['data']['object'] ?? [];
427 $orderId = $paymentIntent['metadata']['order_id'] ?? '';
428
429 if ($orderId) {
430 $order = fluent_cart_get_order($orderId);
431 if ($order) {
432 $this->handlePaymentSuccess($paymentIntent, $order);
433 }
434 }
435 }
436
437 private function handlePaymentFailed($payload)
438 {
439 $paymentIntent = $payload['data']['object'] ?? [];
440 $orderId = $paymentIntent['metadata']['order_id'] ?? '';
441
442 if ($orderId) {
443 $order = fluent_cart_get_order($orderId);
444 if ($order) {
445 $this->handlePaymentFailure($paymentIntent, $order);
446 }
447 }
448 }
449
450 private function handleRefundReceived($payload)
451 {
452 $refund = $payload['data']['object'] ?? [];
453 // Process refund webhook logic here
454 }
455
456 private function handlePaymentSuccess($paymentIntent, $order)
457 {
458 $order->payment_status = 'paid';
459 $order->status = 'processing';
460 $order->vendor_charge_id = $paymentIntent['id'];
461 $order->save();
462
463 do_action('fluent_cart/payment_success', [
464 'order' => $order,
465 'payment_intent' => $paymentIntent
466 ]);
467 }
468
469 private function handlePaymentFailure($paymentIntent, $order)
470 {
471 $order->payment_status = 'failed';
472 $order->status = 'failed';
473 $order->save();
474
475 do_action('fluent_cart/payment_failed', [
476 'order' => $order,
477 'payment_intent' => $paymentIntent
478 ]);
479 }
480 }
481